sshlg-skills 1.63.2 → 1.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,66 @@
1
+ ## v1.64.0 — fewer tokens before the first word; work stops at its finish line
2
+
3
+ - `npx sshlg-skills context` measures what every host loads before the first user word:
4
+ bytes, approximate tokens and lines per instruction file, the hub's blocks against other
5
+ text, and documented caps. It reports a Codex instruction chain over `project_doc_max_bytes`
6
+ (Codex stops adding instruction files there; the default is 32 KiB) as an error and a
7
+ `CLAUDE.md` over Claude Code's 200-line target as a note. Read-only; prints sizes, never text.
8
+ - The routing text may fall and never grow by accident: `routing-registry-total` in DOCMAP is
9
+ checked on every run. The per-prompt routing note is one header line shorter.
10
+ - The wait rule gains the finish line (revision wait-v2): continuing is not polishing — a unit
11
+ ends when its exit checks are green and two rounds found nothing blocking; a finding that
12
+ blocks nothing becomes a board row. It also says a board row addressed to you is a next
13
+ task, never to end a turn on "shall I continue?", to release a lease as soon as the wait
14
+ clears, and lists spend above the limit and the operator's recorded decisions as human gates
15
+ (B-196, B-197). Profiles that applied wait-v1 are upgraded in place, and
16
+ a crash left by a wait-v1 launcher still recovers. A record from a newer launcher names
17
+ `npx --yes sshlg-skills@latest` as the remedy. Launchers up to 1.63.3 cannot read a wait-v2
18
+ receipt; update with `@latest`.
19
+ - task-pipeline is pinned at 1.96.0 (`2674147`): an iteration fixes its finish line at intake,
20
+ triages every finding as blocking, deferred or noise, and stops after two rounds with nothing
21
+ blocking or when its round and release budget is spent; `waves.py finish` computes the verdict.
22
+ - New working rule: [context economy](docs/working-rules/context-economy.md) — what belongs in
23
+ always-loaded instructions, one home per rule, host caps, and stopping by contract.
24
+ - **`blocks install` / `uninstall` / `resolve` — a writer other tools can call for their
25
+ own block (managed-blocks/1).** Preview by default; with `--apply` the host file is
26
+ copied to `~/.sshlg-skills/backups/`, only the named block changes, and every step is
27
+ journaled in `~/.sshlg-skills/managed-blocks/journal.jsonl` (hash-chained, no block
28
+ text). It refuses the hub's own blocks, `SSHLG:REPOSITORY-HANDOFF` (owner undecided,
29
+ B-174), other `SSHLG:` names and an `--owner` that differs from the marker's prefix.
30
+ A `journal.head` beside the journal catches cut-off trailing records; every failure
31
+ answers in one JSON/text shape that names `blocks resolve` when a write may have landed.
32
+ Install adds exactly the block and its line ending (CRLF in CRLF files) and uninstall
33
+ takes exactly those back; a result that would not parse, a read-only file and a
34
+ dropped in-block edit are refused or warned about before the write.
35
+ The convention is `docs/working-rules/managed-blocks.md`. `blocks --json` now names each
36
+ block's registered owner and writer; `OBSERVATORY:AGENT-UPDATES` is registered as
37
+ Observatory's. `blocks --help` and `blocks <verb> --help` answer.
38
+ - **`blocks` ignores markers inside closed Markdown code fences:** an example of a block
39
+ in prose is no longer listed or treated as a stray marker.
40
+ - **The DigitalOcean credential refusal leads with the step that needs no Observatory**
41
+ (a file with mode 600), with the vault named as the alternative. A new suite,
42
+ `test/remedies_test.js`, holds every secret-guard refusal to an Observatory-free step
43
+ before any Observatory step (USR-011); it reads only the remedy part of each refusal, every
44
+ `rule` key, bare or quoted, refusing a spelling it cannot read (a variable, the
45
+ `{ rule }` shorthand), and every deny site of the PreToolUse hook.
46
+
47
+ ## v1.63.3 — pinned sources for every Claude member; blocked members no longer stop the run
48
+
49
+ - A Claude member whose installed bytes already equal the pin is now rebound to the hub's
50
+ pinned catalog: its marketplace source moves to the pin, with no install or uninstall, and
51
+ the run verifies that its install records did not change and its bytes still equal the pin.
52
+ A later native update therefore follows the pin, not upstream (B-191).
53
+ - A member the native contract cannot change — installed only at project or local scope,
54
+ in user plus other scopes with different bytes, or a configured plugin — is reported as
55
+ blocked and keeps its installed version, while the other members and every later channel
56
+ (plain skills, runtime, hooks, Codex) proceed; the result is partial and exits nonzero.
57
+ Malformed install records still stop the whole run (B-192).
58
+ - The hook runtime records which bytes the hub installed (`.managed.json`). A library module a
59
+ later package drops is moved to `.retired/<stamp>/` only while it still has those bytes, and
60
+ `update`/`hooks install` print what moved; hook scripts, edited, foreign or older unrecorded
61
+ files stay where they are (B-193).
62
+ - Inserting into a non-empty object of a tab-indented JSON file keeps tabs (B-194).
63
+
1
64
  ## v1.63.2 — the Claude transaction keeps settings in Claude Code's format
2
65
 
3
66
  - Replacing a marketplace source in `~/.claude/settings.json` wrote the new object on one
package/README.md CHANGED
@@ -44,7 +44,7 @@ scripts. No services, no telemetry, no API keys.
44
44
  | Skill | Version | What it does |
45
45
  |---|---|---|
46
46
  | **[super-ux](https://github.com/ssheleg/super-ux)** | 0.59.3 | Scenario-driven UI development. A versioned design chain in `docs/ux/` — the product vision → personas and jobs → user flows → a screens-and-states map with Figma frames → traced scenarios → evidence-backed audits → fix plans, plus `docs/brand/` for how the product speaks. One `/ux` entry point that reaches every skill, two doc-drift linters and a contract doctor. |
47
- | **[task-pipeline](https://github.com/ssheleg/task-pipeline)** | 1.95.0 | Full-cycle delivery orchestrator. An intake grill interrogates the request into a complete brief, then **ten gated stages** carry it — docs, brainstorm and decompose, spec, plan, build, tests, deploy, post-deploy, wiki, acceptance — refusing to advance until each gate passes. Documentation is a deliverable with its own portable gate, and the retrospective it leaves behind is traceable to the commit that earned each lesson. Ships `project-audit`: a cold-start audit of a whole project — discovery, probes whose unmet needs read `blind` rather than clean, production evidence including one artefact served under two channels, and an HTML report beside a JSON sidecar that says what moved. |
47
+ | **[task-pipeline](https://github.com/ssheleg/task-pipeline)** | 1.96.0 | Full-cycle delivery orchestrator. An intake grill interrogates the request into a complete brief, then **ten gated stages** carry it — docs, brainstorm and decompose, spec, plan, build, tests, deploy, post-deploy, wiki, acceptance — refusing to advance until each gate passes. Documentation is a deliverable with its own portable gate, and the retrospective it leaves behind is traceable to the commit that earned each lesson. Ships `project-audit`: a cold-start audit of a whole project — discovery, probes whose unmet needs read `blind` rather than clean, production evidence including one artefact served under two channels, and an HTML report beside a JSON sidecar that says what moved. |
48
48
  | **[agent-sync](https://github.com/ssheleg/agent-sync)** | 1.22.2 | Several agents, one repository, no collisions. Leases with a TTL so two agents cannot claim the same work, race-free id reservation, a run journal and a generated board — over a pluggable knowledge cloud. The answer to "two sessions just committed over each other". |
49
49
  | **[make-skill](https://github.com/ssheleg/make-skill)** | 0.31.2 | A skill that builds skills. Create, retrofit, audit and publish agent skills and Claude Code plugins: conformance to the [Agent Skills](https://agentskills.io/specification) open standard, [Anthropic's platform rules](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) (per-surface runtime limits, the Skills API, evals) and the [Claude Code plugin reference](https://code.claude.com/docs/en/plugins-reference), marketplace layout, version sync, validator + CI, every distribution channel, the review checklist for third-party skills, and what a *skill author* must know about MCP and A2A — the protocols themselves live in `agent-stack`. |
50
50
  | **[sheleg-design](https://github.com/ssheleg/sheleg-design-skill)** | 1.64.2 | The taste layer. Cinematic scroll-driven landing pages — one scroll clock, motion that degrades to calm, WebGL particle formations — plus product-UI style packs each shipping a ready token layer, and the Figma border: tokens as variables, design to code without hand-copied values. |
@@ -861,6 +861,17 @@ Credentials go through Project Observatory (`observatory_credentials`, `use_secr
861
861
  --vault-only`); the family's secret guard names an Observatory-free route in every refusal
862
862
  as well. Observatory: <https://github.com/passioncode-ai/project-observatory-dashboard>.
863
863
 
864
+ ### What every agent loads — `context`
865
+
866
+ ```bash
867
+ npx sshlg-skills context # per host: bytes, ~tokens, lines, hub-managed vs other text, caps
868
+ npx sshlg-skills context --json
869
+ ```
870
+
871
+ Read-only. Exits 1 when a host file is over a documented cap (Codex stops adding instruction
872
+ files at `project_doc_max_bytes`, 32 KiB by default) and notes a `CLAUDE.md` above Claude Code's
873
+ 200-line target. The rules behind it: [context economy](docs/working-rules/context-economy.md).
874
+
864
875
  ## Other commands
865
876
 
866
877
  ```bash
@@ -1028,3 +1039,23 @@ The source must contain one complete matching outer block. Output contains only
1028
1039
  the selected block's before/after text, so treat that output as local material.
1029
1040
  The command does not apply the diff. Unknown/foreign blocks remain inspectable;
1030
1041
  REPOSITORY-HANDOFF ownership is still a separate operator decision.
1042
+
1043
+ ### Write another tool's block (managed-blocks/1)
1044
+
1045
+ A tool that keeps its own block in a host instruction file — Observatory's
1046
+ `OBSERVATORY:AGENT-UPDATES`, for one — can let the hub write it:
1047
+
1048
+ ```bash
1049
+ npx sshlg-skills blocks install OBSERVATORY:AGENT-UPDATES --owner OBSERVATORY --agent claude --from ./block.md
1050
+ npx sshlg-skills blocks install OBSERVATORY:AGENT-UPDATES --owner OBSERVATORY --agent claude --from ./block.md --apply
1051
+ npx sshlg-skills blocks uninstall OBSERVATORY:AGENT-UPDATES --owner OBSERVATORY --agent claude --apply
1052
+ ```
1053
+
1054
+ Without `--apply` it previews and writes nothing. With it, the file is copied to
1055
+ `~/.sshlg-skills/backups/` first, only the named block changes, and each step is
1056
+ recorded in `~/.sshlg-skills/managed-blocks/journal.jsonl`. It writes `claude`,
1057
+ `codex`, `gemini` and `pi` files, never installs a host, and refuses the hub's own
1058
+ blocks (they have their own writers), `SSHLG:REPOSITORY-HANDOFF` (no owner decided)
1059
+ and any block whose owner prefix differs from `--owner`. An interrupted write is
1060
+ classified with `npx sshlg-skills blocks resolve <operation-id>`. The convention:
1061
+ [docs/working-rules/managed-blocks.md](docs/working-rules/managed-blocks.md).
@@ -293,6 +293,7 @@ Usage:
293
293
  # Pi, OpenCode, Hermes; not Claude/Codex)
294
294
  npx sshlg-skills hooks [status] # what is wired, and what holds it
295
295
  npx sshlg-skills hooks install [--force] [--dry-run]
296
+ npx sshlg-skills context [--json] # what each host loads before the first word; caps
296
297
  npx sshlg-skills hooks recovery inspect [--json]
297
298
  npx sshlg-skills hooks recovery recover --expect <digest|absent> [--json]
298
299
  npx sshlg-skills hooks remove
@@ -312,7 +313,7 @@ Usage:
312
313
  npx sshlg-skills visibility marks [--init --show <owner/repo,…> [--replace] [--apply]]
313
314
  # this machine's pack marks
314
315
  # (~/.sshlg-skills/pack-marks.json)
315
- npx sshlg-skills blocks [--agent claude|codex|gemini|pi|cursor] [--json]\n npx sshlg-skills blocks diff OWNER:NAME --from <local-file> --agent <host>\n # read-only markers and selected local diff\n npx sshlg-skills rules [--apply] [--agent codex|gemini|pi|cursor] [--claude-only]\n # opt-in wait rule; preview unless --apply\n npx sshlg-skills rules recovery inspect [--json]\n npx sshlg-skills rules recovery finalize --operation <id> [--apply] [--json]\n # metadata only; never restores host instructions\n npx sshlg-skills domains [--apply | --revert] [--root <dir>] [--no-usage]
316
+ npx sshlg-skills blocks [--agent claude|codex|gemini|pi|cursor] [--json]\n npx sshlg-skills blocks diff OWNER:NAME --from <local-file> --agent <host>\n # read-only markers and selected local diff\n npx sshlg-skills blocks install|uninstall OWNER:NAME --owner OWNER --agent <host> [--from <file>] [--apply]\n npx sshlg-skills blocks resolve <operation-id>\n # managed-blocks/1 writer for a tool's own block; preview unless --apply\n npx sshlg-skills rules [--apply] [--agent codex|gemini|pi|cursor] [--claude-only]\n # opt-in wait rule; preview unless --apply\n npx sshlg-skills rules recovery inspect [--json]\n npx sshlg-skills rules recovery finalize --operation <id> [--apply] [--json]\n # metadata only; never restores host instructions\n npx sshlg-skills domains [--apply | --revert] [--root <dir>] [--no-usage]
316
317
  npx sshlg-skills domains --enable|--disable <member> [--project <dir>]
317
318
  # xr-dev, web3d-dev, telegram-dev; hook-bearing members stay global
318
319
  # load only in projects that call for them
@@ -407,8 +408,9 @@ function withClaudePlan(steps, f) {
407
408
  steps.unshift(opres.step('claude-pins', 'home',
408
409
  'native immutable source transaction; protected catalogs/settings, scoped repair, rollback on failure, payload readback',
409
410
  { fingerprint: native.fingerprint, profile: native.profile,
410
- members: native.jobs.map(j => `${j.pluginInstall}: ${j.action} @ ${j.row.commit}; ${j.row.files.length} files`)
411
- .concat(native.equivalent.map(e => `${e.pluginInstall}: payload-equivalent, left installed (installed ${String(e.installed).slice(0, 7)}, pin ${e.pin.slice(0, 7)}; ${e.files} files equal)`)),
411
+ members: native.jobs.filter(j => j.action !== 'rebind').map(j => `${j.pluginInstall}: ${j.action} @ ${j.row.commit}; ${j.row.files.length} files`)
412
+ .concat(native.equivalent.map(e => `${e.pluginInstall}: payload-equivalent (installed ${String(e.installed).slice(0, 7)}, pin ${e.pin.slice(0, 7)}; ${e.files} files equal) — source rebind, no reinstall`))
413
+ .concat(native.blocked.map(b => `${b.pluginInstall}: blocked, left as installed — ${b.reason}`)),
412
414
  journal: path.join(native.transactionRoot, 'latest.json') }));
413
415
  }
414
416
  const result = attachWaitPlan(steps, f);
@@ -419,26 +421,31 @@ function runClaudePlan(planned, f) {
419
421
  const saved = claudePlans.get(planned);
420
422
  if (!saved) {
421
423
  log(`\n== Updating Claude Code plugins ==\n skipped: ${claudeScope(f).skippedBy} (the Claude host is outside this run's scope)`);
422
- return true;
424
+ return { ok: true, partial: false };
423
425
  }
424
- for (const e of saved.native.equivalent) log(` ${e.member || e.name}: payload equals pin ${e.pin.slice(0, 7)} (${e.files} files); installed source ${String(e.installed).slice(0, 7)} kept and its marketplace source is unchanged, so a later native update follows that source, not the pin`);
426
+ for (const e of saved.native.equivalent) log(` ${e.member || e.name}: payload equals pin ${e.pin.slice(0, 7)} (${e.files} files); no reinstall — only its marketplace source moves to the pin catalog`);
427
+ for (const b of saved.native.blocked) log(` ${b.name}: blocked, left as installed — ${b.reason}`);
428
+ const clean = !saved.native.blocked.length;
425
429
  if (!saved.native.jobs.length) {
426
430
  // Nothing is written; still describe the state at apply time, not at preview.
427
431
  let again;
428
432
  try { again = require('../lib/claude-pinned').prepare(SKILLS, saved.inventory, { home: os.homedir(), profileRoot: claudeRoot() }); }
429
- catch (e) { log(` Claude native transaction refused before execution: ${e.message}`); return false; }
430
- if (again.fingerprint !== saved.native.fingerprint) { log(' Claude native state changed since the preview; run update again'); return false; }
431
- log(' Claude native transaction: not needed — every member already carries its pinned payload');
432
- return true;
433
+ catch (e) { log(` Claude native transaction refused before execution: ${e.message}`); return { ok: false, partial: false }; }
434
+ if (again.fingerprint !== saved.native.fingerprint) { log(' Claude native state changed since the preview; run update again'); return { ok: false, partial: false }; }
435
+ log(clean ? ' Claude native transaction: not needed — every member already carries its pinned payload'
436
+ : ' Claude native transaction: nothing else to do; blocked members above keep their installed version (partial)');
437
+ return { ok: true, partial: !clean };
433
438
  }
434
439
  // Admission, version check and all backups happen before any other channel.
435
440
  let result;
436
441
  try { result = require('../lib/claude-pin-transaction').execute(saved.native, SKILLS, saved.inventory); }
437
- catch(e) { log(` Claude native transaction refused before execution: ${e.message}`); return false; }
442
+ catch(e) { log(` Claude native transaction refused before execution: ${e.message}`); return { ok: false, partial: false }; }
438
443
  log(` Claude native transaction: ${result.ok ? 'verified immutable payloads' : 'failed; inspect recovery receipt'} — ${result.receipt}`);
439
444
  for (const r of result.results) log(` ${r.member}: ${r.status}${r.rollback ? '; rollback: ' + r.rollback : ''}`);
440
445
  if (result.error) log(` ${result.error}`);
441
- return result.ok;
446
+ if (!clean) log(' Claude: partial — blocked members above keep their installed version');
447
+ // A blocked member makes the result partial; the other channels still run.
448
+ return { ok: result.ok, partial: !clean };
442
449
  }
443
450
 
444
451
  /** The same contract for `update` — every subprocess, prune, router and runtime action. */
@@ -516,8 +523,9 @@ function renderDryRun(planned, mode) {
516
523
  function cmdInstall(f) {
517
524
  const planned = frozenOperations.get(f) || planInstall(f);
518
525
  if (f.dryRun) return renderDryRun(planned, 'install');
519
- if (!runClaudePlan(planned, f)) return false;
520
- let ok = true;
526
+ const claude = runClaudePlan(planned, f);
527
+ if (!claude.ok) return false;
528
+ let ok = !claude.partial;
521
529
  if (!f.claudeOnly) {
522
530
  const agents = skillsCliAgents(f);
523
531
  log(`\n== Installing to agents via skills CLI: ${agents.join(', ')} ==`);
@@ -590,8 +598,9 @@ function scopedRoutingTargets(f) {
590
598
  function cmdUpdate(f) {
591
599
  const planned = frozenOperations.get(f) || planUpdate(f);
592
600
  if (f.dryRun) return renderDryRun(planned, 'update');
593
- if (!runClaudePlan(planned, f)) return false;
594
- let ok = true;
601
+ const claude = runClaudePlan(planned, f);
602
+ if (!claude.ok) return false;
603
+ let ok = !claude.partial;
595
604
  // Count and execute the subprocess entries from the same frozen plan.
596
605
  progress.total =
597
606
  (!f.claudeOnly && fs.existsSync(path.join(ROOT, '.gitmodules')) ? 1 : 0)
@@ -659,6 +668,8 @@ function cmdUpdate(f) {
659
668
  + `${before.differing.length} changed`
660
669
  : ' already level with this package');
661
670
  for (const m of before.missing) log(` + ${m}`);
671
+ if (r.retired && r.retired.length) log(` retired ${r.retired.length} file(s) this package dropped (kept under ${root}/.retired/): ${r.retired.join(', ')}`);
672
+ if (r.ownedError) log(` warning: ${r.ownedError}`);
662
673
  }
663
674
  } catch (e) {
664
675
  // Other update steps may finish, but failed runtime refresh makes the aggregate red.
@@ -3207,6 +3218,30 @@ function cmdMaterialised(argv) {
3207
3218
  return rows.some((r) => !r.satisfied) ? 2 : 0;
3208
3219
  }
3209
3220
 
3221
+ /** Read-only: sizes of the instruction files each host loads, and documented caps. */
3222
+ function cmdContext(argv) {
3223
+ const json = argv.includes('--json');
3224
+ if (argv.some(a => a !== '--json')) { log('usage: npx sshlg-skills context [--json]'); return 2; }
3225
+ const pathMod = require('path'), C = require('../lib/context.js'), A = require('../lib/apply.js'), home = os.homedir();
3226
+ const hosts = A.TARGETS.map(t => C.host(t.agent, pathMod.join(A.hostRoot(t, home), t.file)));
3227
+ const codex = A.TARGETS.find(t => t.agent === 'codex'), codexRoot = A.hostRoot(codex, home);
3228
+ // Codex reads AGENTS.override.md in place of AGENTS.md when it exists.
3229
+ const override = C.host('codex', pathMod.join(codexRoot, 'AGENTS.override.md'));
3230
+ if (override.text != null || override.error) { const main = hosts.find(h => h.agent === 'codex'); if (main && override.text != null) main.shadowed = true; hosts.push(override); }
3231
+ for (const name of ['sshlg-routing.mdc', 'sshlg-wait.mdc']) hosts.push(C.host('cursor', pathMod.join(home, '.cursor', 'rules', name)));
3232
+ const config = C.read(pathMod.join(codexRoot, 'config.toml'));
3233
+ const r = C.report(hosts, config);
3234
+ if (json) { console.log(JSON.stringify(r)); return r.findings.some(f => f.severity === 'error') ? 1 : 0; }
3235
+ log('Always-loaded instruction files (' + r.estimate + '):');
3236
+ for (const row of r.rows) {
3237
+ if (!row.present) { log(` ${row.agent.padEnd(7)} ${row.error ? 'unreadable (' + row.error + ')' : 'absent'} ${row.file}`); continue; }
3238
+ log(` ${row.agent.padEnd(7)} ${String(row.bytes).padStart(7)} B ~${String(row.approxTokens).padStart(6)} tok ${String(row.lines).padStart(5)} lines hub ${row.managedBytes} B · other ${row.operatorBytes} B${row.shadowed ? ' (shadowed by AGENTS.override.md)' : row.cap ? ` cap ${row.cap} (${row.capSource})` : ''}`);
3239
+ }
3240
+ for (const f of r.findings) log(` ${f.severity}: ${f.message}`);
3241
+ if (!r.findings.length) log(' no host is over a documented cap');
3242
+ return r.findings.some(f => f.severity === 'error') ? 1 : 0;
3243
+ }
3244
+
3210
3245
  function cmdHooks(argv) {
3211
3246
  const fs = require('fs');
3212
3247
  const pathMod = require('path');
@@ -3260,6 +3295,8 @@ function cmdHooks(argv) {
3260
3295
  function syncRuntime() {
3261
3296
  const result = require('../lib/runtime.js').sync(pkgRoot, root, { create: true });
3262
3297
  if (result.ok === false) throw new Error(result.reason);
3298
+ if (result.retired && result.retired.length) log(` runtime: retired ${result.retired.length} dropped file(s) to ${root}/.retired/: ${result.retired.join(', ')}`);
3299
+ if (result.ownedError) log(` runtime warning: ${result.ownedError}`);
3263
3300
  return root;
3264
3301
  }
3265
3302
 
@@ -3758,6 +3795,7 @@ function main(argv) {
3758
3795
  // Also before parseFlags: `hooks` takes a positional subcommand.
3759
3796
  if (cmd === 'hooks') { const r = cmdHooks(rest); return typeof r === 'number' ? r : (r ? 0 : 1); }
3760
3797
  if (cmd === 'injectors') { cmdInjectors(); return 0; }
3798
+ if (cmd === 'context') return cmdContext(rest);
3761
3799
  if (cmd === 'conflicts') { cmdConflicts(); return 0; }
3762
3800
  if (cmd === 'toolkit') { return cmdToolkit(argv) || 0; }
3763
3801
  if (cmd === 'visibility') return cmdVisibility(argv);
@@ -1033,8 +1033,8 @@
1033
1033
  {
1034
1034
  "name": "task-pipeline",
1035
1035
  "repo": "ssheleg/task-pipeline",
1036
- "commit": "5a787b4cce4e53aaa76b0971cf9c0293e4dc126b",
1037
- "version": "1.95.0",
1036
+ "commit": "2674147ccfc5eab19aab8f9d7334909cc22f64b2",
1037
+ "version": "1.96.0",
1038
1038
  "pluginInstall": "task-pipeline@task-pipeline",
1039
1039
  "pluginPath": "plugins/task-pipeline",
1040
1040
  "entry": {
@@ -1042,7 +1042,7 @@
1042
1042
  "displayName": "Task Pipeline",
1043
1043
  "source": "./plugins/task-pipeline",
1044
1044
  "description": "Runs a substantial task through a mandatory built-in intake grill, then 10 gated stages (docs, brainstorm+decompose, spec, plan, subagent build, tests, lint/deploy, post-deploy, docs/wiki, acceptance). Every stage's doctrine is built into the skill — no companion plugin required — with typed auto/judgment/manual gates, a frozen requirement spine that closes with evidence, a work board and a verification ledger that outlive a run, an exposure line naming what shipped unconfirmed, a progress rail computed from the project's own config, a loop guard whose review ceiling measures rather than stops, and stage-3 tracks for what a product does, how it sounds and how it looks. Two modes need no task: `checkup` (what is unverified) and `setup` (audit existing docs); a third, `waves`, takes the whole board and closes it in waves that each finish in one pass. Retro insights can publish upstream as issues, opt-in and redacted.",
1045
- "version": "1.95.0",
1045
+ "version": "1.96.0",
1046
1046
  "author": {
1047
1047
  "name": "ssheleg",
1048
1048
  "url": "https://x.com/sshlg93"
@@ -1065,7 +1065,7 @@
1065
1065
  "files": [
1066
1066
  {
1067
1067
  "path": ".claude-plugin/plugin.json",
1068
- "sha256": "787dd14074da0abe882ec2615b13239a4535f73ae09642d2747ac2a95d5e6ef7",
1068
+ "sha256": "9d60330aed0146b81a89a1cb85859610491796ae9a062a619ecef37140e4a659",
1069
1069
  "bytes": 1490
1070
1070
  },
1071
1071
  {
@@ -1260,13 +1260,13 @@
1260
1260
  },
1261
1261
  {
1262
1262
  "path": "skills/task-pipeline/pipeline.example.json",
1263
- "sha256": "b5e7410dfa027c7d0516a87921285bed5e4361b9b7e8ebd872258904d845d715",
1264
- "bytes": 25528
1263
+ "sha256": "4a67e7259e6414c53a269baa579935026c955355ea1090aea575418be05b0069",
1264
+ "bytes": 25596
1265
1265
  },
1266
1266
  {
1267
1267
  "path": "skills/task-pipeline/pipeline.schema.json",
1268
- "sha256": "83e79764e07b2dbc53662e5eb871bf372dedcb653eea22feca8d157129301672",
1269
- "bytes": 18782
1268
+ "sha256": "4ce237089c9c3c507e5d3fa36f093feca4f5289f6d0867debf2645c4959bd9d3",
1269
+ "bytes": 20061
1270
1270
  },
1271
1271
  {
1272
1272
  "path": "skills/task-pipeline/references/acceptance.md",
@@ -1285,8 +1285,8 @@
1285
1285
  },
1286
1286
  {
1287
1287
  "path": "skills/task-pipeline/references/audit.md",
1288
- "sha256": "825f15c2aa62651f577c1c885561cd46e5f2b0c135346da3ca8760195ec1b321",
1289
- "bytes": 25195
1288
+ "sha256": "d6c772b7eb9dde912338b2dc2ae424d4fc77c2e575cf9f824dfd0a5a9642c3f2",
1289
+ "bytes": 25494
1290
1290
  },
1291
1291
  {
1292
1292
  "path": "skills/task-pipeline/references/backlog.md",
@@ -1320,8 +1320,8 @@
1320
1320
  },
1321
1321
  {
1322
1322
  "path": "skills/task-pipeline/references/continuity.md",
1323
- "sha256": "0f02b7af3a0eec9975e59a818e073e7e4113e152dbdaa15d1be94988c4be08a2",
1324
- "bytes": 29284
1323
+ "sha256": "224ade56b7560b3365fc665b7018cefa9ea9d7f58b0b46cb4d3ba4134eb15f49",
1324
+ "bytes": 29721
1325
1325
  },
1326
1326
  {
1327
1327
  "path": "skills/task-pipeline/references/conventions.md",
@@ -1340,8 +1340,8 @@
1340
1340
  },
1341
1341
  {
1342
1342
  "path": "skills/task-pipeline/references/doctrine-map.md",
1343
- "sha256": "7ee53209584e1390929a615124a1b0e48c93f2a8ba818ff1a905f25cb2c4aaf7",
1344
- "bytes": 5047
1343
+ "sha256": "8c2a4ebb23f00068ed351dd890e12cd44eec5df86b5f735f559c1a43cc82e9ea",
1344
+ "bytes": 5187
1345
1345
  },
1346
1346
  {
1347
1347
  "path": "skills/task-pipeline/references/documentation.md",
@@ -1353,6 +1353,11 @@
1353
1353
  "sha256": "cdeab4810e18f42dedcef1f91dac17381f26062028de5ccd81dd9e1e0e1ed1d1",
1354
1354
  "bytes": 7148
1355
1355
  },
1356
+ {
1357
+ "path": "skills/task-pipeline/references/finish-line.md",
1358
+ "sha256": "a63a4e6997451b5ba96aaa140bfaef638349fc657f04c6e18dbbb3b3a7a9139c",
1359
+ "bytes": 6975
1360
+ },
1356
1361
  {
1357
1362
  "path": "skills/task-pipeline/references/gates.md",
1358
1363
  "sha256": "e76d55a8ff93eabf34e12c0e5422c6fb909b87a76b99e86a32d54a7624493ba3",
@@ -1370,8 +1375,8 @@
1370
1375
  },
1371
1376
  {
1372
1377
  "path": "skills/task-pipeline/references/iteration.md",
1373
- "sha256": "7f11287c14373e51a13887b6f7e0dffdbcddada68aa5268e19e736752501a784",
1374
- "bytes": 24990
1378
+ "sha256": "c325d6e0981865966019830a01d292762df1b32dfed44485d2b685fb0bab5c68",
1379
+ "bytes": 25480
1375
1380
  },
1376
1381
  {
1377
1382
  "path": "skills/task-pipeline/references/knowledge-graph.md",
@@ -1390,8 +1395,8 @@
1390
1395
  },
1391
1396
  {
1392
1397
  "path": "skills/task-pipeline/references/loop-guard.md",
1393
- "sha256": "b6252a10a63c76ab965f48e225fa4f9b973948318a0dc66feb6edce0a059cf0b",
1394
- "bytes": 11452
1398
+ "sha256": "1a62dde018bc1783baf6ecd1a808ce46065185007090e275aad8a50e48cbb961",
1399
+ "bytes": 11797
1395
1400
  },
1396
1401
  {
1397
1402
  "path": "skills/task-pipeline/references/model-tiering.md",
@@ -1405,8 +1410,8 @@
1405
1410
  },
1406
1411
  {
1407
1412
  "path": "skills/task-pipeline/references/portability.md",
1408
- "sha256": "45ee554356540073f9f2800914047320acf34d314a6038357fecf881215b158a",
1409
- "bytes": 11794
1413
+ "sha256": "eebbe048ca37387810584d5b9ba5315386b4f4480bedc19e81562650cf8da370",
1414
+ "bytes": 11929
1410
1415
  },
1411
1416
  {
1412
1417
  "path": "skills/task-pipeline/references/prioritisation.md",
@@ -1420,8 +1425,8 @@
1420
1425
  },
1421
1426
  {
1422
1427
  "path": "skills/task-pipeline/references/progress.md",
1423
- "sha256": "0692f07dfe78433ea752d7659d7e6e8383c1b519a43de720946001dc8458cbce",
1424
- "bytes": 25041
1428
+ "sha256": "216f5d195c8b99aa7113d9fd19c6666b88b6b0cadd38eb93c840440fbabbb6eb",
1429
+ "bytes": 25540
1425
1430
  },
1426
1431
  {
1427
1432
  "path": "skills/task-pipeline/references/residue.md",
@@ -1435,8 +1440,8 @@
1435
1440
  },
1436
1441
  {
1437
1442
  "path": "skills/task-pipeline/references/review.md",
1438
- "sha256": "74fdce3c623a3647df69b62509f6bd174534c81bf0f16f8856e14e99596dc71c",
1439
- "bytes": 12720
1443
+ "sha256": "268d1f2394e69a30dafe2050d38a8b9e9f7ae17e30f8a32678f20623c8912942",
1444
+ "bytes": 13073
1440
1445
  },
1441
1446
  {
1442
1447
  "path": "skills/task-pipeline/references/setup.md",
@@ -1505,8 +1510,8 @@
1505
1510
  },
1506
1511
  {
1507
1512
  "path": "skills/task-pipeline/references/stages.md",
1508
- "sha256": "2405a0e0ff14c16d538bc65910c9ed5d034af69bb0c38893dfca016e675b8a06",
1509
- "bytes": 35455
1513
+ "sha256": "8c75b84bf5ad4978329696dca1fcdaf4a2710b6a3a0e1fdc3380393e9107a506",
1514
+ "bytes": 35794
1510
1515
  },
1511
1516
  {
1512
1517
  "path": "skills/task-pipeline/references/tdd.md",
@@ -1520,8 +1525,8 @@
1520
1525
  },
1521
1526
  {
1522
1527
  "path": "skills/task-pipeline/references/waves.md",
1523
- "sha256": "3956d45dae13921432f1b2d06f7c8ec01f5979d5217e9d747b52636dd612d446",
1524
- "bytes": 17363
1528
+ "sha256": "0540ba5a052c1ab69065d6012ebf8ec69a3494fbd229cf7226ba3517af06bfa1",
1529
+ "bytes": 17641
1525
1530
  },
1526
1531
  {
1527
1532
  "path": "skills/task-pipeline/references/work-graph.md",
@@ -1545,7 +1550,7 @@
1545
1550
  },
1546
1551
  {
1547
1552
  "path": "skills/task-pipeline/scripts/graph.py",
1548
- "sha256": "32564f76954273a97fbd10b3fa92f63a0d4f8d5eea48a8078f7451529065c280",
1553
+ "sha256": "f07abf42a455676a7ca8e733bcaab371cecedb29fb16f594895b9c15a1b4b736",
1549
1554
  "bytes": 111655
1550
1555
  },
1551
1556
  {
@@ -1575,13 +1580,13 @@
1575
1580
  },
1576
1581
  {
1577
1582
  "path": "skills/task-pipeline/scripts/waves.py",
1578
- "sha256": "a6e4b2edcbebb563dd5dfdb7fb2f41d9e4ee50d54210d2561655a1ef9bdc3711",
1579
- "bytes": 26094
1583
+ "sha256": "937bd11092e0765cde2bda4f84a4932f056fc8b3cad8233678241e24f3a18223",
1584
+ "bytes": 39837
1580
1585
  },
1581
1586
  {
1582
1587
  "path": "skills/task-pipeline/SKILL.md",
1583
- "sha256": "16cebba6aa7de43178fd68e462b4ada2852fef0290e0593006e055d3f43f60b4",
1584
- "bytes": 20796
1588
+ "sha256": "3054cbd174efd2779df62facb2b199ffda275e34eb0d7d39f3abb834596c19c3",
1589
+ "bytes": 21033
1585
1590
  },
1586
1591
  {
1587
1592
  "path": "skills/task-pipeline/templates/adr.md",
@@ -1600,8 +1605,8 @@
1600
1605
  },
1601
1606
  {
1602
1607
  "path": "skills/task-pipeline/templates/brief.md",
1603
- "sha256": "c876537cd73072c68f8b54d7f9576f7fd83a51663f84dd1ebd410d4a947780c5",
1604
- "bytes": 11032
1608
+ "sha256": "1338ed76ca9d2a8cc0c180799871fad6771401c415a37064e498b69daa5ea84b",
1609
+ "bytes": 11765
1605
1610
  },
1606
1611
  {
1607
1612
  "path": "skills/task-pipeline/templates/browser-claims.json",
@@ -1670,8 +1675,8 @@
1670
1675
  },
1671
1676
  {
1672
1677
  "path": "skills/task-pipeline/templates/inventory.schema.json",
1673
- "sha256": "30bee56cca5fe470674c53836e959389963aedb3eefb0e1b528b9cf6efe23621",
1674
- "bytes": 5637
1678
+ "sha256": "2b5e183c6f36aa9f47ceb0fa56c964dc3cc08872b046e07d02e3d23c5bb2a9ea",
1679
+ "bytes": 7613
1675
1680
  },
1676
1681
  {
1677
1682
  "path": "skills/task-pipeline/templates/lesson.schema.json",
@@ -1710,8 +1715,8 @@
1710
1715
  },
1711
1716
  {
1712
1717
  "path": "skills/task-pipeline/templates/run.md",
1713
- "sha256": "f6e4882f48b9ed62245ffe0183d57b20894e44257d057b79aaddaad2d1d3d4c5",
1714
- "bytes": 11154
1718
+ "sha256": "9bfe093a68c5836afa268207dbbae3f8cdb8cf5dcbe14368c3e91dd7c643b6b4",
1719
+ "bytes": 13007
1715
1720
  },
1716
1721
  {
1717
1722
  "path": "skills/task-pipeline/templates/stage-coverage.sh",
package/lib/blocks.js CHANGED
@@ -4,13 +4,39 @@ const fs=require('fs'),path=require('path'),crypto=require('crypto');
4
4
  const SCOPE=[['claude','CLAUDE_CONFIG_DIR','.claude','CLAUDE.md'],['codex','CODEX_HOME','.codex','AGENTS.md'],['gemini','GEMINI_CONFIG_DIR','.gemini','GEMINI.md'],['pi','PI_CODING_AGENT_DIR','.pi/agent','AGENTS.md'],['cursor',null,'.cursor/rules','sshlg-routing.mdc'],['cursor',null,'.cursor/rules','sshlg-wait.mdc']];
5
5
  const MAX_BYTES=1024*1024;
6
6
  function decode(bytes){const s=bytes.toString('utf8');if(!Buffer.from(s,'utf8').equals(bytes))throw Error('INVALID_UTF8');return s;}
7
- function admission(id){if(id==='SSHLG:REPOSITORY-HANDOFF')return 'OPERATOR_DECISION_REQUIRED';if(id==='SSHLG:ROUTERS'||id==='SSHLG:RULE:wait-is-not-a-stop'||subregion(id))return 'REGISTERED_NOT_AUTHORIZED';return 'FOREIGN';}
7
+ // managed-blocks/1 ownership (docs/working-rules/managed-blocks.md § Block names). The
8
+ // registry names who writes a block; it never authorizes the hub to write a foreign one.
9
+ const REGISTRY={
10
+ 'SSHLG:ROUTERS':{owner:'sshlg-skills',writer:'npx sshlg-skills install/update (lib/apply.js)',dedicated:true},
11
+ 'SSHLG:RULE:wait-is-not-a-stop':{owner:'sshlg-skills',writer:'npx sshlg-skills rules (lib/wait-rules.js)',dedicated:true},
12
+ 'SSHLG:REPOSITORY-HANDOFF':{owner:null,writer:null,undecided:'B-174: the operator has not decided which tool owns it'},
13
+ 'OBSERVATORY:AGENT-UPDATES':{owner:'project-observatory',writer:'agent_updates.py through npx sshlg-skills blocks install --owner OBSERVATORY'},
14
+ };
15
+ function admission(id){if(id==='SSHLG:REPOSITORY-HANDOFF')return 'OPERATOR_DECISION_REQUIRED';if(REGISTRY[id]||subregion(id))return 'REGISTERED_NOT_AUTHORIZED';return 'FOREIGN';}
16
+ function registered(id){const r=REGISTRY[id]||(subregion(id)?REGISTRY['SSHLG:ROUTERS']:null);return r?{owner:r.owner,writer:r.writer}:null;}
8
17
  function subregion(id){return /^SSHLG:ROUTER:[a-z0-9-]+$/.test(id)||/^SSHLG:ROUTERS:(MAP|PROTOCOL|TABLE)$/.test(id);}
18
+ // Closed Markdown code fences (``` or ~~~, up to three spaces of indent). A marker inside
19
+ // one is an example in prose, not a block: it is neither listed nor written. An unclosed
20
+ // fence fences nothing, so it can never hide a real block's END.
21
+ function fences(text){
22
+ const out=[];let open=null,at=0;
23
+ for(const line of text.split('\n')){
24
+ const m=/^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line.replace(/\r$/,''));
25
+ if(!open){if(m&&!(m[1][0]==='`'&&m[2].includes('`')))open={ch:m[1][0],len:m[1].length,start:at};}
26
+ else if(m&&m[1][0]===open.ch&&m[1].length>=open.len&&!m[2].trim()){out.push([open.start,at+line.length]);open=null;}
27
+ at+=line.length+1;
28
+ }
29
+ return out;
30
+ }
9
31
  function parse(text){
10
32
  const blocks=[],issues=[],stack=[],seen=new Set();const tokens=[];const re=/<!--[\s\S]*?(?:-->|$)/g;let match;
33
+ const fenced=fences(text);let f=0;const inFence=at=>{while(f<fenced.length&&fenced[f][1]<at)f++;return f<fenced.length&&fenced[f][0]<=at&&at<=fenced[f][1];};
11
34
  const starts=[0];for(let i=text.indexOf('\n');i!==-1;i=text.indexOf('\n',i+1))starts.push(i+1);
12
35
  const line=at=>{let lo=0,hi=starts.length;while(lo<hi){const mid=(lo+hi)>>>1;if(starts[mid]<=at)lo=mid+1;else hi=mid;}return lo;};
13
36
  while((match=re.exec(text))){
37
+ // A comment opened inside a fence ends with the fence: resume scanning after it, so an
38
+ // unterminated `<!--` in an example cannot swallow a real marker below.
39
+ if(inFence(match.index)){re.lastIndex=fenced[f][1];continue;}
14
40
  const body=match[0].slice(4);if(!/[^\s<>:]+\s*:\s*[^\s<>:]+\s*:\s*(?:BEGIN|END|OPTOUT)\b/i.test(body))continue;
15
41
  const m=/^[ \t]*([A-Za-z0-9_.-]+(?::[A-Za-z0-9_.-]+)+):(BEGIN|END|OPTOUT)(?=[ \t\r\n]|-->)/.exec(body);
16
42
  if(!m||!match[0].endsWith('-->')||m[1].length>128){issues.push({code:'MALFORMED_MARKER',line:line(match.index)});continue;}
@@ -22,7 +48,7 @@ function parse(text){
22
48
  if(t.kind==='BEGIN'||t.kind==='OPTOUT'){
23
49
  const nested=stack.length>0;const allowed=nested&&stack.length===1&&stack[0].id==='SSHLG:ROUTERS'&&subregion(t.id)&&t.kind==='BEGIN';
24
50
  const bad=seen.has(t.id)||(nested&&!allowed)||(!nested&&subregion(t.id));
25
- const b={markerId:t.id,range:{startLine:t.line,endLine:t.kind==='OPTOUT'?t.endLine:null},syntaxState:bad?'AMBIGUOUS':t.kind==='OPTOUT'?'OPTOUT':allowed?'NESTED_SUBREGION':'PAIRED',claimedOwner:t.id.split(':')[0],writerAdmission:admission(t.id),lastJournalState:'NOT_READ'};
51
+ const b={markerId:t.id,range:{startLine:t.line,endLine:t.kind==='OPTOUT'?t.endLine:null},syntaxState:bad?'AMBIGUOUS':t.kind==='OPTOUT'?'OPTOUT':allowed?'NESTED_SUBREGION':'PAIRED',claimedOwner:t.id.split(':')[0],registered:registered(t.id),writerAdmission:admission(t.id),lastJournalState:'NOT_READ'};
26
52
  // Offsets are private parser data and never part of public JSON.
27
53
  Object.defineProperty(b,'offset',{value:{start:t.start,end:t.kind==='OPTOUT'?t.end:null},enumerable:false});
28
54
  if(bad)issues.push({code:'AMBIGUOUS_MARKER',line:t.line});seen.add(t.id);blocks.push(b);
@@ -90,5 +116,11 @@ function localDiff(options){
90
116
  const current=matches[0];const before=current.r.text.slice(current.b.offset.start,current.b.offset.end),after=proposed.text.slice(pb.offset.start,pb.offset.end);
91
117
  return before===after?'No block changes.\n':['--- current selected block','+++ proposed selected block',...before.split('\n').map(x=>'-'+x),...after.split('\n').map(x=>'+'+x),'Read-only: no bytes changed; no write authority inferred.',''].join('\n');
92
118
  }
93
- function cli(args,options,write){const f=flags(args);if(f.diff){write(localDiff({...options,...f}));return 0;}const r=inspect({...options,...f});write(f.json?JSON.stringify(r,null,2)+'\n':r.targets.map(t=>`${t.host} ${t.pathAlias}: ${t.readState}${t.blocks?' ('+t.blocks.length+' markers; '+(t.ok?'syntax valid':'syntax uncertain')+')':''}`).join('\n')+'\nRead-only metadata; marker ownership does not authorize writes.\n');return r.ok?0:1;}
94
- module.exports={parse,decode,read,inspect,flags,localDiff,cli,targets};
119
+ const USAGE=`npx sshlg-skills blocks [--agent claude|codex|gemini|pi|cursor] [--json] # list markers, read-only
120
+ npx sshlg-skills blocks diff OWNER:NAME --from <local-file> --agent <host> # selected local diff, read-only
121
+ `;
122
+ function cli(args,options,write){
123
+ if(['install','uninstall','resolve'].includes(args[0]))return require('./blockwriter.js').cli(args,options,write);
124
+ if(args.includes('--help')||args.includes('-h')){write(USAGE+require('./blockwriter.js').USAGE);return 0;}
125
+ const f=flags(args);if(f.diff){write(localDiff({...options,...f}));return 0;}const r=inspect({...options,...f});write(f.json?JSON.stringify(r,null,2)+'\n':r.targets.map(t=>`${t.host} ${t.pathAlias}: ${t.readState}${t.blocks?' ('+t.blocks.length+' markers; '+(t.ok?'syntax valid':'syntax uncertain')+')':''}`).join('\n')+'\nRead-only metadata; marker ownership does not authorize writes.\n');return r.ok?0:1;}
126
+ module.exports={parse,decode,read,inspect,flags,localDiff,cli,targets,REGISTRY};