feat: auto-protect sensitive workspace files on init#10
Open
LanderXT wants to merge 10 commits into
Open
Conversation
ipman init now writes missing .gitignore entries for ipman.db, keysalt, and ipman.db.bak, and injects an ipman section into .claude/CLAUDE.md so agents can verify the invariant. Both operations are best-effort and non-fatal. The init JSON response surfaces gitignore_updated and an agent_instructions array for downstream verification. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- UserPromptSubmit hook re-injects project instructions before every prompt via <project_instructions> XML tags, combating context decay in long sessions (system-reminder pattern used by Claude Code itself) - CLAUDE.md gains a "Project instructions" section with rationale so agents loading it at session start understand why and when to apply them - ipman -N now wraps the project instructions block in XML tags and adds a Priority column, making critical vs normal constraints visible without typographic emphasis (per Anthropic/OpenAI prompting guides) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a `priority` column ('critical' | 'normal', default 'normal') to
the instructions table via migration 0007. Only project-scoped
instructions accept a non-default priority; the op layer enforces this
so the constraint can be relaxed later without a schema change.
- migration 0007: table rebuild with CHECK (priority IN ('critical','normal'))
- instruction.add / instruction.update: accept and validate priority param
- instruction.list: surfaces priority in every returned row
- workspace.context_get: includes priority in context.project.instructions
- agent_docs.c: updated schema docs and entity description
- test 210: covers priority round-trip, invalid value rejection, and
enforcement that non-project scopes cannot set priority
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a workspace rule requiring every code-touching task.close to reference its git commit via a `<plan.code>/T<local_seq>: ...` subject prefix and a `commit: <sha>` line in outcome_summary. Surfaces the operational pattern as Section 7 "Commit linkage" in SKILL.md (renumbers Error Codes → 8, Agent Handoff → 9) and updates the Task Lifecycle example to demonstrate commit-then-close. Motivation: prior plans (#11–#13) closed tasks without committing, leaving the ipman task state and git history out of sync. The new policy makes the link explicit; the existing local_seq + plan.code infrastructure (migrations 0016/0017) supplies the marker components.
`run_next` used to exit 1 unconditionally on the no-active-plan path, even when the project block (tools / env_vars / project-scoped instructions) had rendered useful content to stdout. Callers like Claude Code treat exit ≠ 0 as "call failed; discard output," which silently drops the critical project instructions the workspace was trying to surface. Change `next_print_project` to return a count of rendered subsections (banner doesn't count) and gate the exit code on that count: - ≥1 subsection rendered → exit 0 (caller gets actionable content) - 0 subsections rendered → exit 1 (truly empty workspace, legacy hint still fires on stderr) The plan-active path (`next_render`) ignores the return value and exits 0 as before. Verified: tests/integration/090_cli_next.sh still passes (its no-plan case uses `|| true` and only asserts on the stderr hint). Targeted sweep on a fresh workspace confirms case A (empty project) → exit 1, case B (project instruction added) → exit 0.
…able Today cli_table computes column width as max(header, max_cell_len) with no upper bound. A single long cell (e.g. a multi-paragraph project_instructions body) blows the entire table out to thousands of columns, mangling box-drawing chars in any non-infinite terminal. Add an optional per-column cap: - `cli_table_set_col_max_width(t, col, max_width)` enables wrapping for one column; 0 (default) keeps the legacy unlimited behavior so every existing caller is unaffected. - `cli_table_add_row` respects the cap when computing col_width, but the cell content is stored unmodified; wrapping happens at render time. - `print_data_row` wraps each cell into N visual lines and emits the logical row as max(N over cells) visual lines, padding columns whose cells ran out. Both embedded `\n` (hard breaks) and width-based word-wrapping are honored. - Wrapping is skipped for cells containing any byte >= 0x80 to avoid splitting a UTF-8 codepoint mid-byte (pre-existing strlen-based width math is byte-oriented; codepoint-aware wrapping is a separate change). Verified: - make test passes (no regression in any integration test that renders tables: 030, 040, 090, 120, etc.). - Direct smoke test with a 20-column cap exercised three rows (short, long single paragraph, paragraph with embedded blank line); all rendered with correctly aligned borders and word-boundary wrapping.
…umn padding) T2's wrap_cell skipped wrapping entirely when any byte >= 0x80 appeared in a cell, returning the full text as one line. Combined with print_data_row's defensive byte-length truncation, cells containing even a single non-ASCII codepoint (e.g. an em-dash in a project instruction body) were silently cut off at col_width bytes — only the first 80 bytes survived for a 1000+ byte multi-paragraph instruction. Replace the ASCII-only fast path with a codepoint walk: * utf8_seq_len(c) returns the 1–4 byte sequence length for a UTF-8 lead byte; invalid lead bytes (continuation, 0xF8+) return 1 so the walker always makes forward progress. * utf8_display_cols(s) counts codepoints (one column per codepoint — doesn't model wide CJK or zero-width chars; matches existing strlen-based col_width math for ASCII while keeping multi-byte cells visually aligned). * wrap_cell now walks codepoints, tracks display columns on the current visual line, and wraps at the rightmost ASCII space within the width window — falling back to a hard break at the next codepoint boundary if no space is available. Codepoints are never split mid-byte. * print_data_row pads cells in display columns (utf8_display_cols) instead of bytes, so UTF-8 cells align with ASCII cells in the same column. The defensive truncation is removed: when a line genuinely exceeds col_width (only possible in the width=0 fallback or on alloc failure), the cell overflows visually rather than hiding content. Verified: - make test green (full suite). - Direct smoke test wraps three rows (ASCII, em-dash mid-line, and multi-paragraph with em-dash) cleanly at a 30-col cap; all paragraph boundaries preserved, all codepoints intact. This unblocks T3 (cap the project_instructions body column at 80 cols) which was deferred because the previous implementation would have truncated the existing body content at the first em-dash.
The Body column in the project_instructions table was unbounded. Combined with a single multi-paragraph critical-priority instruction, the table rendered as a 1206-byte-wide row that mangled box-drawing characters in any non-infinite terminal and showed Claude Code a visually broken handoff view. Apply the cap added in NXFIX/T2 (cli_table_set_col_max_width); UTF-8 wrapping from NXFIX/T5 keeps em-dashes and other multibyte chars intact at codepoint boundaries. Verified end-to-end: `ipman -N` against the current workspace renders the full 1174-byte project instruction wrapped across 17 visual lines inside an 82-column-wide column, with paragraph breaks preserved as blank lines and em-dashes rendered correctly.
Discovered during NXFIX/T4 verification: the Plan/Phase/Task cursor detail tables (Field/Value) share the same unbounded-width problem that NXFIX/T3 fixed for project_instructions. A long plan Summary or Description produced 283-byte rows because the Value column had no cap. One-line wiring of `cli_table_set_col_max_width(&t, 1, 80)` before the `add_str_row` calls. UTF-8 codepoint wrap from NXFIX/T5 handles em-dashes correctly. Verified: `make test` green, `ipman -N` against the current workspace now shows the Plan detail table bounded at 95 display columns (down from 283), with multi-line Summary wrapped at word boundaries.
`plan_from_row` in src/plan_ops.c selects `code` as column 1 of the
SELECT, but the helper never adds it to the JSON output — so every
plan op that returns a plan (plan.get, plan.create, plan.update,
plan.close, plan.reopen, etc.) was hiding a field that both the
schema (migration 0017 makes code mandatory, auto-generated `P<id>`)
and the docs (agent_docs.c:163 lists code among standard plan fields)
treat as first-class.
This worked around itself in one specific place: `next_print_cursor`
prefers code over label for the cursor banner heading, but with code
missing from the response it always fell back to label. The 090 test
even codified the fallback in a comment ("v2.0 dropped code from plan
responses, so next_print_cursor falls back to label"). Now that the
NXFIX/NXCUR commit-hygiene policy uses `<plan.code>/T<local_seq>` as
the canonical commit-message marker, hiding code on every read is
actively user-hostile: agents need code on every plan touch to build
markers without an extra plan.lookup call.
Changes:
- src/plan_ops.c: add ipman_json_add_text_or_null(plan, "code", col 1)
to plan_from_row, next to label since they are sibling handles.
- tests/integration/010_schema_allowlist_behavior.sh: flip the
absence-assertion to confirm the supplied code round-trips
("TST-001"). Updated comment to explain why code rides along.
- tests/integration/090_cli_next.sh: cursor plan banner now uses
`NX-1` (the supplied code) instead of `next-plan` (the label
fallback); replaced the v2.0-era comment with one describing the
new behavior.
Verified: `make test` green; `./build/ipman` plan.get on plans 3/4/5
returns the supplied codes (NXFIX, NXCUR, NXCODE) where it previously
returned null.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ipman initnow appends missing entries (.ipman/ipman.db,.ipman/keysalt,.ipman/ipman.db.bak) to the repo's.gitignore, preventing secrets and binary blobs from reaching git history.claude/CLAUDE.md(idempotent via<!-- ipman-managed -->marker) so agents are aware of the gitignore invariantgitignore_updated(bool) andagent_instructions[].verify_gitignoreso downstream agents can assert the invariant was appliedTest plan
ipman initin a fresh repo →.gitignorecontains all three entries;.claude/CLAUDE.mdcontains the ipman sectionipman init→ no duplicate entries written (idempotency)ipman initin a repo where.gitignorealready has the entries →gitignore_updated: falsein responseipman initwith no write permission to repo root → init succeeds, error is silently swallowed🤖 Generated with Claude Code