@zalom/plastic 2.0.0-alpha.22 → 2.0.0-alpha.24
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/PLASTIC.md +6 -5
- package/agents/plastic-enforcer.md +6 -3
- package/agents/plastic-executor.md +4 -0
- package/agents/plastic-node-research.md +28 -0
- package/agents/plastic-node-verify.md +27 -0
- package/agents/plastic-node-work.md +32 -0
- package/bin/lib/context_budget.rb +1 -1
- package/hooks/hooks.json +12 -0
- package/hooks/statusline +28 -0
- package/hooks/stop +5 -0
- package/package.json +1 -1
- package/scripts/dashboard.rb +1 -1
- package/scripts/doctor.rb +80 -6
- package/scripts/end-intent +22 -19
- package/scripts/exec-worktree +5 -5
- package/scripts/graph-measure +249 -0
- package/scripts/hook-call-budget +6 -6
- package/scripts/hook-capture +16 -15
- package/scripts/hook-record +21 -14
- package/scripts/hook-savepoint +26 -3
- package/scripts/hook-session-start +49 -10
- package/scripts/hook-stop +57 -0
- package/scripts/lib/active_delivery.rb +105 -0
- package/scripts/lib/agent_models.rb +10 -1
- package/scripts/lib/arm.rb +41 -102
- package/scripts/lib/codex_adapter.rb +197 -0
- package/scripts/lib/{packet_wrapper.rb → data_boundary.rb} +7 -7
- package/scripts/lib/day_summary.rb +19 -11
- package/scripts/lib/doctor_core.rb +8 -3
- package/scripts/lib/doctor_session_ledger.rb +3 -52
- package/scripts/lib/engine_permissions.rb +88 -0
- package/scripts/lib/exec_worktree.rb +24 -21
- package/scripts/lib/graph_edges.rb +4 -4
- package/scripts/lib/graph_file.rb +4 -4
- package/scripts/lib/graph_measure.rb +645 -0
- package/scripts/lib/graph_measure_budget.rb +409 -0
- package/scripts/lib/graph_measure_cohorts.rb +487 -0
- package/scripts/lib/graph_measure_models.rb +411 -0
- package/scripts/lib/graph_measure_report.rb +532 -0
- package/scripts/lib/graph_tree.rb +2 -2
- package/scripts/lib/handoff.rb +40 -13
- package/scripts/lib/harness_adapter.rb +184 -0
- package/scripts/lib/hook_registry.rb +13 -1
- package/scripts/lib/hook_replay.rb +23 -5
- package/scripts/lib/index_entry.rb +53 -0
- package/scripts/lib/index_projection.rb +1 -1
- package/scripts/lib/insights.rb +1 -1
- package/scripts/lib/installer_core.rb +228 -9
- package/scripts/lib/intent_screen.rb +1 -1
- package/scripts/lib/intent_validator.rb +2 -2
- package/scripts/lib/lock.rb +8 -9
- package/scripts/lib/meter_watch.rb +15 -9
- package/scripts/lib/node_file.rb +3 -3
- package/scripts/lib/{node_packet.rb → node_input.rb} +49 -49
- package/scripts/lib/node_input_compatibility.rb +62 -0
- package/scripts/lib/node_ledger.rb +11 -2
- package/scripts/lib/node_progress.rb +153 -0
- package/scripts/lib/outcome_report.rb +2 -2
- package/scripts/lib/project_config.rb +45 -0
- package/scripts/lib/ready_set.rb +1 -1
- package/scripts/lib/report_screen.rb +10 -6
- package/scripts/lib/roadmap_graph.rb +1 -1
- package/scripts/lib/roadmap_queue.rb +1 -1
- package/scripts/lib/roadmap_render.rb +1 -1
- package/scripts/lib/roadmap_savepoint.rb +1 -1
- package/scripts/lib/runner_absorb.rb +36 -10
- package/scripts/lib/runner_dispatch.rb +66 -51
- package/scripts/lib/runner_sweep.rb +4 -4
- package/scripts/lib/runner_until_empty.rb +252 -0
- package/scripts/lib/runner_watch.rb +389 -0
- package/scripts/lib/savepoint.rb +9 -10
- package/scripts/lib/scaffold_intent.rb +6 -3
- package/scripts/lib/session_close.rb +30 -28
- package/scripts/lib/session_git.rb +22 -16
- package/scripts/lib/session_ledger.rb +44 -4
- package/scripts/lib/stop_gate.rb +95 -0
- package/scripts/lib/verify_intent.rb +2 -2
- package/scripts/lib/work_graph_validator.rb +6 -6
- package/scripts/lib/worktree.rb +24 -24
- package/scripts/lib/worktree_sweep.rb +3 -3
- package/scripts/new-intent +1 -1
- package/scripts/{node-packet → node-input} +15 -15
- package/scripts/node-run +224 -0
- package/scripts/plastic-lock +33 -32
- package/scripts/read-config +6 -0
- package/scripts/runner +203 -19
- package/scripts/verify-intent +1 -1
- package/skills/auto/SKILL.md +8 -8
- package/skills/auto/references/end-tail.md +9 -11
- package/skills/conventions/references/completion-and-done.md +9 -10
- package/skills/conventions/references/knowledge-graph.md +9 -0
- package/skills/conventions/references/locks-and-worktrees.md +10 -12
- package/skills/direct/SKILL.md +2 -2
- package/skills/doctor/SKILL.md +4 -4
- package/skills/intent-creating/evals/evals.json +1 -1
- package/skills/intent-executing/SKILL.md +7 -1
- package/skills/releasing/SKILL.md +2 -2
- package/skills/releasing/references/promotion-and-tagging.md +1 -1
- package/skills/releasing/references/release-lines.md +1 -1
- package/skills/tutorial/references/track-2-auto.md +1 -1
- package/templates/index.md +1 -1
- package/templates/project.yml +1 -1
- package/scripts/lib/bridge.rb +0 -116
|
@@ -4,10 +4,10 @@ This chapter holds the delivery lock, claims, worktrees, the fail-safe doctrine,
|
|
|
4
4
|
|
|
5
5
|
### Delivery Isolation and the Single-Owner Lock
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
The delivery lock names the session delivering an intent, as its owner or a delegate. Locks
|
|
8
|
+
and worktrees exist only for auto teams: an interactive session working direct or thinking
|
|
9
|
+
takes no lock and records into its day ledger instead (the oldest day in the last seven whose
|
|
10
|
+
checklist carries the session's line, else today).
|
|
11
11
|
|
|
12
12
|
For an auto team, exactly one team develops an intent's delivery at a time. Ownership is
|
|
13
13
|
session-keyed and durable: arming acquires `delivery.lock` inside the intent directory
|
|
@@ -17,9 +17,7 @@ but never grants access and is never inferred from transcripts or filesystem pat
|
|
|
17
17
|
fields on legacy locks display as `Unknown`. Liveness is a lease: the record hook refreshes
|
|
18
18
|
the lock file's mtime on every write the owning session makes, and that mtime is the sole
|
|
19
19
|
heartbeat truth. The lock counts as stale only when the mtime is older than the TTL. No
|
|
20
|
-
process id is consulted anywhere. The
|
|
21
|
-
records into; the lock file is the truth of who owns a delivery, and wins on any
|
|
22
|
-
disagreement. Another team that finds a fresh lock backs off; a stale lock is reclaimed only
|
|
20
|
+
process id is consulted anywhere. The lock file is the truth of who owns a delivery. Another team that finds a fresh lock backs off; a stale lock is reclaimed only
|
|
23
21
|
by explicit takeover, which replaces the lock and appends an audit line to the intent's
|
|
24
22
|
savepoint.md. Rearming the same session preserves its acquired identity and refreshes known
|
|
25
23
|
provenance; an explicit takeover replaces the controller and starts new provenance.
|
|
@@ -31,7 +29,7 @@ retained as descriptive history, bounded to the 20 most recent terminal entries.
|
|
|
31
29
|
a delegate, and an artifact claim are distinct evidence: controller ownership authorizes the
|
|
32
30
|
delivery, delegate registration authorizes a child session, and a claim selects one current
|
|
33
31
|
writer for one artifact. Disarm clears the lock; the End tail is ordered: verify, merge and
|
|
34
|
-
remove worktrees, clear the lock
|
|
32
|
+
remove worktrees, then clear the lock. Repair
|
|
35
33
|
is one idempotent function with two entry points: the `plastic-lock` command (`who`, status,
|
|
36
34
|
fix, release, reclaim, delegate) and the `plastic-doctor` skill's lock section, so repair
|
|
37
35
|
self-heals. `who` is read-only and reports the controller, mtime heartbeat, delegates, and
|
|
@@ -90,18 +88,18 @@ an orphaned worktree behind, and clear a stale worktree reference with `git work
|
|
|
90
88
|
|
|
91
89
|
#### Intent delivery, station by station
|
|
92
90
|
|
|
93
|
-
How one auto-team intent travels from boarding to the End tail, and what the lock
|
|
91
|
+
How one auto-team intent travels from boarding to the End tail, and what the lock and
|
|
94
92
|
the record hook do at each station. Nothing in the third column blocks; the fourth column is
|
|
95
93
|
what gets written down.
|
|
96
94
|
|
|
97
|
-
| Station | Delivered artifact | Lock
|
|
95
|
+
| Station | Delivered artifact | Lock steps | Record |
|
|
98
96
|
|---|---|---|---|
|
|
99
|
-
| Start (board) | none (a procedure, not a stage) | `plastic-lock fix` self-heals stale, corrupt, or legacy state; arm acquires `delivery.lock` (O_EXCL, session-keyed), provisions the code worktree
|
|
97
|
+
| Start (board) | none (a procedure, not a stage) | `plastic-lock fix` self-heals stale, corrupt, or legacy state; arm acquires `delivery.lock` (O_EXCL, session-keyed), provisions the code worktree | savepoint confirms the boarding station |
|
|
100
98
|
| What (create) | `<id>--<slug>.md`, born complete | no lock yet; `new-intent` validates the file it writes (`scripts/validate-intent`) | savepoint `What` line; intent listed in INDEX `## Active` |
|
|
101
99
|
| Why | `spec.md` | owner writes refresh the lease (lock file mtime heartbeat) | savepoint `Why started`, `Why spec.md created` |
|
|
102
100
|
| How | `plan.md`, `actions/ACTION_N.md` (at least one), `checklist.md` | heartbeat on writes | savepoint `How started`, `How plan.md created`, `How checklist.md created`, `Exec started` |
|
|
103
101
|
| Exec | code on the intent branch, checklist checked off | heartbeat; code edits confined to the provisioned worktree; delegates write under the owner's lock | checklist boxes; savepoint milestones; the day-ledger line promotes when a project file lands |
|
|
104
|
-
| End (done) | mandatory `outcome.md` (`disposition: delivered\|abandoned`), INDEX moves to Completed or Abandoned | ordered End tail: verify, merge and remove worktrees, disarm clears `delivery.lock`,
|
|
102
|
+
| End (done) | mandatory `outcome.md` (`disposition: delivered\|abandoned`), INDEX moves to Completed or Abandoned | ordered End tail: verify, merge and remove worktrees, disarm clears `delivery.lock`, and the QMD reindex runs LAST; `end-intent` backfills a placeholder `outcome.md` from the record and its structure check reports (never refuses) | the savepoint's terminal `delivered` (or `abandoned`) line; takeover audits, if any, remain in savepoint.md |
|
|
105
103
|
| Maintenance (Future, Terminal, or Active-with-a-stale-or-no-lock) | `revisions.md` move-and-record entries | detects (never acquires) `delivery.lock`; defers and reports while the target's lock is FRESH (`Lock.fresh?`); a stale or absent lock is not-active, maintenance proceeds | append-only, rule-tagged `revisions.md` entry written in the same operation as the change, or the change is refused; lands via a fresh branch off store main merged back as one closed op, never `git add -A` |
|
|
106
104
|
|
|
107
105
|
## The write guard is not residue
|
package/skills/direct/SKILL.md
CHANGED
|
@@ -54,8 +54,8 @@ the 15 observable signals and the response each one selects.
|
|
|
54
54
|
item.
|
|
55
55
|
- Only a prompt that changes something on disk or produces an artifact becomes a checklist item.
|
|
56
56
|
A pure question is answered inline and recorded nowhere.
|
|
57
|
-
- Direct work records into the day ledger
|
|
58
|
-
|
|
57
|
+
- Direct work records into the day ledger for the session's day (the oldest day in the last
|
|
58
|
+
seven whose checklist carries the session's line, else today). Never write that lookup by hand.
|
|
59
59
|
|
|
60
60
|
## 5. What direct does not take
|
|
61
61
|
|
package/skills/doctor/SKILL.md
CHANGED
|
@@ -223,7 +223,7 @@ knowingly-exempt `(intent_id, rule)` pairs. Format: one `rule_name id id id` lin
|
|
|
223
223
|
blank lines and `#` comments ignored. v1 honors exactly one rule, `savepoint_operational`.
|
|
224
224
|
|
|
225
225
|
**Reading the count.** When any exclusion applies, the `savepoint_operational` check's message
|
|
226
|
-
|
|
226
|
+
includes the count and the file's path, e.g. `"... (3 excluded via ~/.plastic/doctor-exclusions)"`.
|
|
227
227
|
A malformed line in the file forces the check to `warn` with the parse error in `details`, even
|
|
228
228
|
when zero real gaps remain, so a broken file is never silently permissive.
|
|
229
229
|
|
|
@@ -262,13 +262,13 @@ Locks exist for auto teams: a `delivery.lock` file in the intent directory names
|
|
|
262
262
|
session, and the `record` hook refreshes its mtime on every edit (the lease heartbeat; stale
|
|
263
263
|
means older than the TTL). Direct work takes no lock. When a lock reads held by a session
|
|
264
264
|
that is gone, when work resumes after a crash, reboot, or `/tmp` wipe, or when the user says
|
|
265
|
-
"fix the lock", "who holds the lock", or "reclaim the lock", use the CLI (intent 304
|
|
265
|
+
"fix the lock", "who holds the lock", or "reclaim the lock", use the CLI (intent 304 merged
|
|
266
266
|
the former locking skill here):
|
|
267
267
|
|
|
268
268
|
| Verb | What it does | When |
|
|
269
269
|
|---|---|---|
|
|
270
270
|
| `who` | Owner, heartbeat, claims, delegates, from durable files only | Safe inspection; needs `--intent-dir` |
|
|
271
|
-
| `status` | Lock file, freshness, the derived worktree, whether this session
|
|
271
|
+
| `status` | Lock file, freshness, the derived worktree, whether this session is delivering the intent, claims | Always safe; run first |
|
|
272
272
|
| `fix` | Idempotent repair from disk truth for this session; never touches a fresh foreign lock | Interrupted work, corrupt state, `/tmp` wiped |
|
|
273
273
|
| `release` | The owner clears the lock | Ending or abandoning an auto delivery |
|
|
274
274
|
| `reclaim` | Explicit takeover of a stale lock; appends an audit line to `savepoint.md` | The owner is gone and the lease expired |
|
|
@@ -290,7 +290,7 @@ these verbs (claims, worktrees, the station ledger).
|
|
|
290
290
|
## Provisioning a project store
|
|
291
291
|
|
|
292
292
|
When a project is registered in `~/.plastic/projects.yml` but has no store on disk (doctor
|
|
293
|
-
reports `project_store_dir`), provision it (intent 304
|
|
293
|
+
reports `project_store_dir`), provision it (intent 304 merged the former provisioning skill
|
|
294
294
|
here). The slug is the project's key under `projects`; an unregistered slug exits non-zero and
|
|
295
295
|
creates nothing, and this procedure never edits `projects.yml`.
|
|
296
296
|
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"scope": "behavior",
|
|
36
36
|
"set": "validation",
|
|
37
37
|
"prompt": "Create an intent that is the direct continuation of intent 41: it emerged from intent 41's lifecycle and could not exist without it.",
|
|
38
|
-
"expected_output": "Because the new intent was genuinely CREATED FROM 41 (D1), it carries 41 in --sources (or branches from 41, which
|
|
38
|
+
"expected_output": "Because the new intent was genuinely CREATED FROM 41 (D1), it carries 41 in --sources (or branches from 41, which merges 41 into sources via the redundant-explicit rule). The reciprocal I1 backlink lands: intent 41's frontmatter chain gains the new intent's id. This is the created-from case, contrasted with the related-but-not-spawned case in eval 1.",
|
|
39
39
|
"files": [],
|
|
40
40
|
"assertions": [
|
|
41
41
|
{
|
|
@@ -26,7 +26,7 @@ touching the worktree above.
|
|
|
26
26
|
|
|
27
27
|
`ruby scripts/runner step <intent_dir>` computes which nodes in `graph.md`/`nodes/*.md` are
|
|
28
28
|
ready, applies dispatch policy (model, call cap), and prints a spawn block per dispatched node
|
|
29
|
-
- agent, model,
|
|
29
|
+
- agent, model, node input path, the one test command, the call cap - fenced in its own stdout.
|
|
30
30
|
The runner itself never spawns an agent (327 D42). Call `step` again after each dispatched
|
|
31
31
|
node returns.
|
|
32
32
|
|
|
@@ -35,6 +35,12 @@ node returns.
|
|
|
35
35
|
The dispatch step is the paste, not a lead's hand-typed brief: copy each spawn block into the
|
|
36
36
|
Agent tool as its own dispatch, verbatim.
|
|
37
37
|
|
|
38
|
+
On Claude Code, the session spawns each dispatched node as a background subagent of its
|
|
39
|
+
per-kind agent (`plastic-node-work`, `plastic-node-verify`, `plastic-node-research`) named in
|
|
40
|
+
the spawn block. On Codex, one node runs over `codex exec` in a sandbox scoped to its kind and
|
|
41
|
+
writes only a return file, which the next `step` absorbs the same way it absorbs a Claude Code
|
|
42
|
+
return.
|
|
43
|
+
|
|
38
44
|
## status
|
|
39
45
|
|
|
40
46
|
`ruby scripts/runner status <intent_dir>` renders the graph's ledger state: which nodes are
|
|
@@ -293,11 +293,11 @@ branch, then removes the worktree, and prunes the repo:
|
|
|
293
293
|
|
|
294
294
|
```bash
|
|
295
295
|
ruby -r ~/.plastic/scripts/lib/worktree -r ~/.plastic/scripts/lib/arm -e \
|
|
296
|
-
'Worktree.finish(Arm.
|
|
296
|
+
'Worktree.finish(Arm.delivery(intent_dir: "<STORE>/<dir>"), merge: true)'
|
|
297
297
|
```
|
|
298
298
|
|
|
299
299
|
(The worktree block is derived from `projects.yml` and the intent id, so the one-liner needs
|
|
300
|
-
only the intent directory
|
|
300
|
+
only the intent directory.)
|
|
301
301
|
|
|
302
302
|
Honor the worktree-cleanup rule: never leave an orphaned worktree, and run `git worktree
|
|
303
303
|
prune` in the affected repo if you hit a stale reference. For why this is the one place the
|
|
@@ -13,7 +13,7 @@ for why the intent's worktree is merged before removal.
|
|
|
13
13
|
## Worktree merge-then-remove rationale
|
|
14
14
|
|
|
15
15
|
**Worktree-isolated intents (intent 73c3).** When the intent was delivered in a Plastic
|
|
16
|
-
worktree (the
|
|
16
|
+
worktree (the delivery lock has a provisioned `worktree` block), its code lives on the branch
|
|
17
17
|
`plastic/{id}--{slug}` inside `<repo>/.claude/worktrees/{id}--{slug}`, not on a hand-made
|
|
18
18
|
feature branch. The merge-then-remove of that worktree is handled together with cleanup in
|
|
19
19
|
Workflow step 9, which merges `plastic/{id}--{slug}` into the default branch BEFORE removing the
|
|
@@ -29,7 +29,7 @@ Work rides the beta-verified lane when it changes operational substrate, carries
|
|
|
29
29
|
migration, lock, or state-format risk, or cannot be fully validated by a hermetic suite alone.
|
|
30
30
|
Everything else merges straight to main.
|
|
31
31
|
|
|
32
|
-
Intent 41's DB layer is the archetypal beta-lane case: it replaces the
|
|
32
|
+
Intent 41's DB layer is the archetypal beta-lane case: it replaces the lock and session
|
|
33
33
|
file formats with a new persistent SQLite substrate. A green suite proves the code correct; it
|
|
34
34
|
cannot prove the new substrate survives real, uncontrolled usage, so real-use verification on
|
|
35
35
|
beta comes first.
|
|
@@ -67,7 +67,7 @@ No new command. At each stage boundary (What, Why, How, Exec) the agent briefs i
|
|
|
67
67
|
fixed three-line shape: State (what happened and why it matters), Risk (the one thing that
|
|
68
68
|
could bite, or "nothing flagged"), and Call (the decision left to the user, or the call the
|
|
69
69
|
agent is taking on its own). That is the depth for a medium or large intent. A small intent
|
|
70
|
-
gets one briefing, at How,
|
|
70
|
+
gets one briefing, at How, merging in what the earlier stages would have said.
|
|
71
71
|
|
|
72
72
|
Checkpoint: in the most recent report, point at the State line, the Risk line, and the Call
|
|
73
73
|
line.
|
package/templates/index.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Active
|
|
4
4
|
<!-- Line shape: `- [ID — Title](path)`. Writes emit the em-dash (U+2014) between
|
|
5
|
-
id and title; readers (
|
|
5
|
+
id and title; readers (IndexEntry.match) accept the em-dash or a plain
|
|
6
6
|
hyphen, so either form parses (see https://github.com/zalom/plastic/blob/main/docs/internals.md). -->
|
|
7
7
|
(no active intents)
|
|
8
8
|
|
package/templates/project.yml
CHANGED
|
@@ -8,5 +8,5 @@ release:
|
|
|
8
8
|
# mode: direct # direct | pull_request
|
|
9
9
|
# base: main # defaults to origin/HEAD, then main, then master
|
|
10
10
|
# branch_template: "session/{{day}}" # {{day}}, {{ticket}}, {{slug}} tokens
|
|
11
|
-
# ticket_source: intent_id # intent_id (the
|
|
11
|
+
# ticket_source: intent_id # intent_id (the delivering intent's id from its delivery lock, else the day id)
|
|
12
12
|
# workspace: checkout # checkout | worktree (worktree applies to direct mode only)
|
package/scripts/lib/bridge.rb
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# encoding: UTF-8
|
|
3
|
-
|
|
4
|
-
require "yaml"
|
|
5
|
-
require_relative "lock"
|
|
6
|
-
|
|
7
|
-
# Bridge - the shared helpers that outlived the /tmp bridge JSON.
|
|
8
|
-
#
|
|
9
|
-
# Until 2.0 this file cached a session's delivery state in
|
|
10
|
-
# `<tmp>/plastic-<session>--<id>.json` (stage, worktree, lock, auto flag) and
|
|
11
|
-
# arbitrated which bridge a gate hook should read. Ruling 6 of intent 296
|
|
12
|
-
# retired that: the per-session pointer (`~/.plastic/store/.tmp/<session>/current`)
|
|
13
|
-
# plus `delivery.lock` in the intent directory is the whole bridge, and
|
|
14
|
-
# `scripts/lib/arm.rb` is how a team takes and gives back an intent (intent
|
|
15
|
-
# 307). What stays here is the handful of pure helpers a dozen callers still
|
|
16
|
-
# share: the INDEX entry matcher, the Active check, the id-from-dir parser,
|
|
17
|
-
# the project config reader, `blank?`, and the skill-reference delegator.
|
|
18
|
-
module Bridge
|
|
19
|
-
# Single place a skill-reference string gets built (intent 201, D3). The
|
|
20
|
-
# prefix table lives on Lock; this is a thin delegator so call sites read
|
|
21
|
-
# Bridge.skill_ref.
|
|
22
|
-
def self.skill_ref(name, harness: :claude)
|
|
23
|
-
Lock.skill_ref(name, harness: harness)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def self.blank?(value)
|
|
27
|
-
value.nil? || value.to_s.strip.empty?
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# --- Shared INDEX entry matcher (intent 188, D12/D13) -----------------------
|
|
31
|
-
#
|
|
32
|
-
# ONE definition site for the "- [ID <sep> Title](link)" shape both
|
|
33
|
-
# `intent_active?` (below) and `scripts/end-intent`'s own INDEX-move parser
|
|
34
|
-
# depend on, so the two regexes can never drift apart. Accepts a real em dash
|
|
35
|
-
# (U+2014) OR a plain hyphen as the id/title separator on READ; every WRITE
|
|
36
|
-
# still emits the real em dash (D10). The separator is built from the
|
|
37
|
-
# codepoint, not a literal byte, so this file stays em-dash free.
|
|
38
|
-
EM_DASH = "\u2014".freeze
|
|
39
|
-
INDEX_ENTRY_RE = /\A- \[(\S+)\s+(?:#{Regexp.escape(EM_DASH)}|-)\s+(.*?)\]\(([^)]+)\)/.freeze
|
|
40
|
-
|
|
41
|
-
# Match `line` (already chomped) against the shared INDEX entry shape.
|
|
42
|
-
# Returns a MatchData (captures: 1 = id, 2 = title, 3 = link) or nil.
|
|
43
|
-
def self.index_entry_match(line)
|
|
44
|
-
line.to_s.match(INDEX_ENTRY_RE)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# True iff the intent is Active in its store's INDEX.md, which lives at the
|
|
48
|
-
# PARENT of the store/ dir. Non-raising: any failure (missing or unreadable
|
|
49
|
-
# INDEX, bad arg) returns false. `index_active_ids` is a pure-data test
|
|
50
|
-
# seam: when an Array of id strings is supplied, membership is checked
|
|
51
|
-
# against it directly with no file read.
|
|
52
|
-
def self.intent_active?(intent_id, store:, index_active_ids: nil)
|
|
53
|
-
target = intent_id.to_s
|
|
54
|
-
return index_active_ids.include?(target) if index_active_ids.is_a?(Array)
|
|
55
|
-
|
|
56
|
-
index = File.join(File.dirname(store.to_s), "INDEX.md")
|
|
57
|
-
return false unless File.exist?(index)
|
|
58
|
-
|
|
59
|
-
in_active = false
|
|
60
|
-
File.foreach(index) do |line|
|
|
61
|
-
stripped = line.chomp
|
|
62
|
-
if stripped == "## Active"
|
|
63
|
-
in_active = true
|
|
64
|
-
next
|
|
65
|
-
end
|
|
66
|
-
next unless in_active
|
|
67
|
-
break if stripped.start_with?("## ") # next section ends the Active block
|
|
68
|
-
m = index_entry_match(stripped)
|
|
69
|
-
return true if m && m[1] == target
|
|
70
|
-
end
|
|
71
|
-
false
|
|
72
|
-
rescue StandardError
|
|
73
|
-
false
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# --- Project config -------------------------------------------------------------
|
|
77
|
-
|
|
78
|
-
PROJECT_CONFIG_DEFAULTS = {
|
|
79
|
-
"governing_docs" => ["AGENTS.md"],
|
|
80
|
-
"release" => {
|
|
81
|
-
"on_complete" => "commit",
|
|
82
|
-
},
|
|
83
|
-
}.freeze
|
|
84
|
-
|
|
85
|
-
def self.read_project_config(slug)
|
|
86
|
-
path = File.join(Dir.home, ".plastic", "projects", slug, "project.yml")
|
|
87
|
-
config = if File.exist?(path)
|
|
88
|
-
YAML.safe_load(File.read(path)) || {}
|
|
89
|
-
else
|
|
90
|
-
{}
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
deep_merge(PROJECT_CONFIG_DEFAULTS, config)
|
|
94
|
-
rescue => e
|
|
95
|
-
$stderr.puts "Warning: failed to read project config for #{slug}: #{e.message}"
|
|
96
|
-
PROJECT_CONFIG_DEFAULTS.dup
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# "<id>" from a ".../store/<id>--<slug>" dir, else nil.
|
|
100
|
-
def self.intent_id_from_dir(dir)
|
|
101
|
-
base = File.basename(dir.to_s)
|
|
102
|
-
base.include?("--") ? base.split("--", 2).first : nil
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def self.deep_merge(base, overlay)
|
|
106
|
-
result = base.dup
|
|
107
|
-
overlay.each do |key, value|
|
|
108
|
-
if value.is_a?(Hash) && result[key].is_a?(Hash)
|
|
109
|
-
result[key] = deep_merge(result[key], value)
|
|
110
|
-
else
|
|
111
|
-
result[key] = value
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
result
|
|
115
|
-
end
|
|
116
|
-
end
|