Skip to content

feat: auto-protect sensitive workspace files on init#10

Open
LanderXT wants to merge 10 commits into
mainfrom
feat/init-gitignore-claude-md
Open

feat: auto-protect sensitive workspace files on init#10
LanderXT wants to merge 10 commits into
mainfrom
feat/init-gitignore-claude-md

Conversation

@LanderXT
Copy link
Copy Markdown
Owner

@LanderXT LanderXT commented May 8, 2026

Summary

  • ipman init now 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
  • Injects a managed section into .claude/CLAUDE.md (idempotent via <!-- ipman-managed --> marker) so agents are aware of the gitignore invariant
  • Init JSON response gains gitignore_updated (bool) and agent_instructions[].verify_gitignore so downstream agents can assert the invariant was applied
  • Both operations are best-effort and non-fatal — a permission error does not abort init

Test plan

  • ipman init in a fresh repo → .gitignore contains all three entries; .claude/CLAUDE.md contains the ipman section
  • Re-run ipman init → no duplicate entries written (idempotency)
  • ipman init in a repo where .gitignore already has the entries → gitignore_updated: false in response
  • ipman init with no write permission to repo root → init succeeds, error is silently swallowed

🤖 Generated with Claude Code

LanderXT and others added 3 commits May 8, 2026 00:35
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>
LanderXT added 7 commits May 18, 2026 14:07
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant