@windyroad/itil 3.4.0-preview.1368 → 3.4.0-preview.1369

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "3.4.0-preview.1368",
3
+ "version": "3.4.0-preview.1369",
4
4
  "description": "ITIL-aligned IT service management for Claude Code and Codex",
5
5
  "bin": {
6
6
  "windyroad-itil": "./bin/install.mjs"
@@ -156,7 +156,7 @@ Use this contribution lifecycle:
156
156
 
157
157
  A starting battle plan exposes likely work, dependencies and parallel opportunities without precreating, assigning or reserving every future task. The increment owner keeps acceptance, sequence and integration clear and resolves overlap/dependencies. Explicit assignments and handoffs remain valid. Two agents sharing an account are distinct contributors. Checklists suffice for routine steps; do not create a task for every command. Task creation within assessed scope requires no fresh human approval.
158
158
 
159
- **Each agent holds only one contribution task at a time**, including a blocked assignment. Finish, explicitly hand over or release the current assignment before claiming another. Do not reserve queued tasks. Outcome or increment coordination ownership does not reserve contribution tasks. Agents sharing an account remain distinct through their recorded agent/session identities.
159
+ **One contribution claim is a ceiling, not a retention requirement.** A blocked assignment still counts until its release is verified. For an unfinished task, when no useful authorized in-task step remains, hand over the remaining gap, remove your assignment and read back its removal; do not retain it merely because its dependency is unresolved. Preserve genuine assigned watcher duties; routine discussion reads alone do not justify retaining an exhausted reservation. Finish, explicitly hand over or release the current assignment before claiming another. Do not reserve queued tasks. Outcome or increment coordination ownership does not reserve contribution tasks. Agents sharing an account remain distinct through their recorded agent/session identities.
160
160
 
161
161
  Before declaring a contribution ready to start, complete this claim receipt: **no other contribution assignment held; parent increment; current assessed plan revision; non-overlapping scope; dependencies (or explicitly none); actual account; distinct agent/session; saved claim and reciprocal-link readback; competing-claim recheck.** Record all fields on the task, even when some are already known in conversation. Assignment does not increase authority. When explaining a proposed claim, make these required fields and the before/after overlap checks explicit.
162
162
 
@@ -188,21 +188,31 @@ actual acknowledgement.
188
188
 
189
189
  At each work checkpoint, reconcile tracking in this order:
190
190
 
191
- For a join/resume checkpoint with a selected Project and working `gh`, MUST use
192
- the [executable Project checkpoint](github-project-checkpoint.md). Read its
193
- inspection and handle current discussion before applying it; do not manually
194
- publish synchronization-success text on that route. Other checkpoints and
195
- authorized alternatives retain the rules below.
196
-
197
- 1. **Establish scope.** Reuse the established backend. Establish Project selection
198
- separately from the backend, using configuration, project requirements or
199
- explicit direction: selected/required, none, or unresolved. Do not infer none
200
- or an unset configuration from omitted information. An established choice to
201
- use no Project needs no setup. While selection remains unresolved, inspect available
202
- records and keep Project work conditional; do not assert a pending or
203
- unsynchronized mirror. An unavailable API or bootstrap default proves neither
204
- selection nor a missing view, field or membership; inspect saved setup before
205
- naming gaps.
191
+ When the affected records include an outcome, current Now, provisional Next and
192
+ a task, and `gh` works, MUST first use the [read-only hierarchy inspector](github-project-checkpoint.md#inspect-the-hierarchy).
193
+ Construct its request from those actual records and consume its findings before
194
+ claiming completeness; identical draft/remote text is not that comparison.
195
+ Do not manufacture a Next issue for this route. Its configuration report is one
196
+ source for the selection decision below, not proof of capability or acceptance.
197
+
198
+ 1. **Establish selection before checking Project capability.** Reuse the
199
+ established backend. From configuration, project requirements or explicit
200
+ direction, choose the applicable branch:
201
+ - **Selected/required:** verify the repository, Project and account, then
202
+ inspect its saved setup and applicable operations. For join/resume of an
203
+ active open task with working `gh`, MUST use the [executable Project checkpoint](github-project-checkpoint.md).
204
+ Read its inspection and handle current discussion before applying it;
205
+ do not manually publish synchronization-success text on that route.
206
+ If an applicable operation is unavailable, retain its exact pending state
207
+ under the recovery rules below. An unavailable API or bootstrap default
208
+ does not prove a missing view, field or membership.
209
+ - **Established none:** continue issue-side reconciliation without Project
210
+ setup, fields or synchronization.
211
+ - **Unresolved:** inspect available configuration, requirements, direction and
212
+ issue records. Record the selection gap and keep Project work conditional;
213
+ there is no established mirror operation to report pending or unsynchronized.
214
+ Omitted information and API availability establish neither selection nor
215
+ an explicit choice to use no Project.
206
216
  2. **Inspect each affected record.** Read each outcome, increment and task
207
217
  against the minimum-information table above. Preserve its own owner/account,
208
218
  applicable agent/session, revision and acceptance; mark missing required
@@ -1,6 +1,72 @@
1
- # Execute a selected-Project resume checkpoint
1
+ # Inspect and reconcile GitHub records
2
2
 
3
- For a join/resume checkpoint with a selected GitHub Project and working `gh`,
3
+ Resolve `scripts/github-project-checkpoint.mjs` relative to this installed
4
+ planning skill, including when called from `contribute`. Use the existing
5
+ authenticated `gh` account and saved project configuration. The default is
6
+ `.agents/planning.json`; an existing project-declared configuration path may be
7
+ supplied last. These operations require no new grants, account changes or tokens.
8
+ Local tracking keeps its existing path.
9
+
10
+ ## Inspect the hierarchy
11
+
12
+ When reconciling an outcome, current Now, an affected provisional Next and a
13
+ task together with working `gh`, planning and `contribute` MUST first use the
14
+ read-only hierarchy inspector. Supply those actual distinct issue numbers;
15
+ do not invent a Next issue to satisfy the command. Other record sets retain
16
+ their existing coordination path.
17
+
18
+ ```json
19
+ {
20
+ "outcome": 1,
21
+ "increment": {"issue": 2},
22
+ "next": {"issue": 3},
23
+ "task": {"issue": 4}
24
+ }
25
+ ```
26
+
27
+ Run `node <installed-planning-skill>/scripts/github-project-checkpoint.mjs inspect-hierarchy request.json`.
28
+ This reads the four actual issues and their returned comments. It writes
29
+ nothing and makes no Project or native-link request. Read the raw records and
30
+ consume `findings` before claiming that the records are correct:
31
+
32
+ - `projectSelection` reports configuration facts. `not-recorded` means selection
33
+ is unresolved from that configuration, not an explicit choice to use no
34
+ Project. `recorded` does not prove capability; consider actual project
35
+ requirements and explicit direction too. Preserve invalid configuration.
36
+ - Each record's `inspection` reports literal owner, revision, acceptance and
37
+ Parent candidates with raw line evidence. `candidate` is text, not verified
38
+ ownership or passed acceptance. `explicitly-unresolved` preserves a recorded
39
+ unknown. Empty/missing literals, unsupported syntax and multiple candidates
40
+ remain unestablished by this inspector; they do not prove semantic information
41
+ is absent or that the issue is invalid. Inspect the actual prose/history
42
+ rather than forcing a new format or choosing a historical value.
43
+ - `textLinks` compares the three requested textual relationships. A mention is
44
+ observed text, not proof of a current native relationship. Inspect both sides
45
+ and repair known reciprocal gaps under the existing coordination rules.
46
+ - `capabilities` are `not-probed`; do not turn that into an outage or native-link
47
+ absence. `inspection-unavailable` means the read/report operation failed,
48
+ not that a Project mirror is pending.
49
+
50
+ A successful command produces a diagnostic report, never a correctness or
51
+ synchronization verdict. Identical drafts and saved bodies do not establish
52
+ completeness. Before claiming completeness, account for every finding through
53
+ actual semantic review, authorized repair with saved readback where needed, or
54
+ honest unresolved recording in the existing plan or record. Follow the selection
55
+ finding's conditional next action, which targets the existing outcome issue
56
+ for saving and reading back a still-unresolved gap. Reuse an already saved gap
57
+ without an unchanged write. Configuration alone may leave the choice unresolved
58
+ while requirements or explicit direction establish it. Unsupported
59
+ syntax does not force normalization or another artifact.
60
+ Apply the existing per-record rules to known gaps, marking missing
61
+ required information unresolved rather than inventing it; preserve completed
62
+ acceptance and attribution. Read back authorized changes and compare reciprocal
63
+ links before reporting reconciliation. Unsupported syntax needs actual review,
64
+ not automatic normalization. Semantic acceptance remains with the caller and
65
+ required independent review.
66
+
67
+ ## Synchronize selected Project fields
68
+
69
+ For a join/resume checkpoint on an active open task with a selected GitHub Project and working `gh`,
4
70
  planning and `contribute` MUST use the bundled
5
71
  `scripts/github-project-checkpoint.mjs` operation. Do not manually publish a
6
72
  Project synchronization-success claim on this route. The operation reads the
@@ -8,10 +74,8 @@ records/discussion, updates the two items, verifies their saved fields and only
8
74
  then generates the task's synchronization result. It does not allocate owners,
9
75
  approve a plan, acknowledge another person or complete the increment.
10
76
 
11
- Resolve the script relative to this installed planning skill, including when
12
- called from `contribute`. Use the existing authenticated `gh` account and saved
13
- project configuration; no new grants, account changes or tokens are required by
14
- the operation. Local and GitHub Issues-only tracking keep their existing paths.
77
+ This mutation route applies to its open outcome, increment and active task.
78
+ GitHub Issues-only tracking uses no Project field mutations.
15
79
 
16
80
  ## Inspect, decide, apply
17
81
 
@@ -26,10 +26,14 @@ function json(args, input) {
26
26
  if (value.errors?.length) throw new Error(value.errors.map(error => error.message).join('; '));
27
27
  return value;
28
28
  }
29
- function scope(request, config) {
30
- requireValue(config.version === 1 && config.backend === 'github', 'This operation requires the saved GitHub backend.');
29
+ function repositoryFor(config) {
30
+ requireValue(config?.version === 1 && config.backend === 'github', 'This operation requires the saved GitHub backend.');
31
31
  const repository = config.github?.repository;
32
- requireValue(typeof repository === 'string' && /^[\w.-]+\/[\w.-]+$/.test(repository), 'Invalid configured GitHub repository.');
32
+ requireValue(typeof repository === 'string' && /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\/[\w.-]+$/.test(repository) && !['.', '..'].includes(repository.split('/')[1]), 'Invalid configured GitHub repository.');
33
+ return repository;
34
+ }
35
+ function scope(request, config) {
36
+ const repository = repositoryFor(config);
33
37
  const url = new URL(config.github?.projectUrl);
34
38
  const match = url.pathname.match(/^\/(?:users|orgs)\/([\w.-]+)\/projects\/([1-9]\d*)(?:\/views\/[1-9]\d*)?\/?$/);
35
39
  requireValue(url.protocol === 'https:' && url.hostname === 'github.com' && match && !url.username && !url.password, 'A selected GitHub Project URL is required.');
@@ -44,6 +48,66 @@ function scope(request, config) {
44
48
  function issue(number, bound) {
45
49
  return json(['issue', 'view', String(number), '--repo', bound.repository, '--json', 'number,url,body,state,assignees,labels,comments']);
46
50
  }
51
+ function briefField(body, pattern, cue) {
52
+ // shortcut: observed literal brief fields only; extend when an actual brief needs another syntax.
53
+ const lines = body.split(/\r?\n/).map((text, index) => ({ line: index + 1, text }));
54
+ const matches = lines.flatMap(line => {
55
+ const match = line.text.match(pattern);
56
+ return match ? [{ ...line, value: match[1].trim() }] : [];
57
+ });
58
+ const literalEvidence = matches.map(({ line, text }) => ({ line, text }));
59
+ if (matches.length > 1) return { state: 'multiple-candidates', evidence: literalEvidence };
60
+ if (matches.length === 1) {
61
+ const { value } = matches[0];
62
+ return { state: !value ? 'empty' : /^(?:unresolved|unknown|tbd|unclaimed)(?:\b|$)/i.test(value) ? 'explicitly-unresolved' : 'candidate', value, evidence: literalEvidence };
63
+ }
64
+ const evidence = lines.filter(line => cue.test(line.text));
65
+ return { state: evidence.length ? 'unsupported-format' : 'literal-field-missing', evidence };
66
+ }
67
+ function textReference(body, number, repository) {
68
+ const escaped = repository.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
69
+ const reference = new RegExp(`(?:(?<![\\w./#:-])#${number}\\b|https://github\\.com/${escaped}/issues/${number}(?![\\w/]))`, 'i');
70
+ const evidence = body.split(/\r?\n/).flatMap((text, index) => reference.test(text) ? [{ line: index + 1, text }] : []);
71
+ return { observed: evidence.length > 0, evidence };
72
+ }
73
+ function inspectHierarchy(request, config) {
74
+ const repository = repositoryFor(config);
75
+ const roles = { outcome: request?.outcome, increment: request?.increment?.issue, next: request?.next?.issue, task: request?.task?.issue };
76
+ requireValue(Object.values(roles).every(n => Number.isSafeInteger(n) && n > 0) && new Set(Object.values(roles)).size === 4, 'Four distinct outcome, increment, next and task issue numbers are required.');
77
+ const projectSelection = { state: 'not-recorded', source: 'configuration' };
78
+ if (Object.hasOwn(config.github, 'projectUrl')) {
79
+ try {
80
+ const url = new URL(config.github.projectUrl);
81
+ requireValue(url.protocol === 'https:' && url.hostname === 'github.com' && !url.username && !url.password && !url.search && !url.hash && /^\/(?:users|orgs)\/[\w.-]+\/projects\/[1-9]\d*(?:\/views\/[1-9]\d*)?\/?$/.test(url.pathname), 'Invalid recorded Project URL.');
82
+ Object.assign(projectSelection, { state: 'recorded', url: `https://github.com${url.pathname.split('/views/')[0].replace(/\/$/, '')}` });
83
+ } catch { projectSelection.state = 'invalid'; }
84
+ }
85
+ const records = {};
86
+ const findings = projectSelection.state === 'recorded' ? [] : [{ issue: roles.outcome, field: 'projectSelection', state: projectSelection.state, nextAction: `Establish selection from applicable project requirements and explicit direction. If it remains unresolved, save that selection gap and conditional Project work on outcome #${roles.outcome} and read back its saved body before claiming reconciliation complete. Reuse an already saved gap without an unchanged write.` }];
87
+ for (const [role, number] of Object.entries(roles)) {
88
+ const record = issue(number, { repository });
89
+ requireValue(record.number === number && record.url?.toLowerCase() === `https://github.com/${repository}/issues/${number}`.toLowerCase() && typeof record.body === 'string', 'Returned issue does not match its requested repository/number or brief.');
90
+ const inspection = {
91
+ owner: briefField(record.body, /^\s*(?:(?:Outcome|Increment|Task) )?Owner:\s*(.*)$/i, /\bowner\b/i),
92
+ revision: briefField(record.body, /^\s*Revision:\s*(.*)$/i, /\brevision\b/i),
93
+ acceptance: briefField(record.body, /^\s*(?:Tentative )?Acceptance(?: criteria| evidence)?:\s*(.*)$/i, /\bacceptance\b/i),
94
+ parent: briefField(record.body, /^\s*Parent(?: outcome| increment)?:\s*(.*)$/i, /\bparent\b/i)
95
+ };
96
+ records[role] = { ...record, inspection };
97
+ for (const field of ['owner', 'revision', 'acceptance']) {
98
+ if (!['candidate', 'explicitly-unresolved'].includes(inspection[field].state)) findings.push({ issue: number, field, state: inspection[field].state });
99
+ }
100
+ }
101
+ const textLinks = [[records.outcome, records.increment], [records.outcome, records.next], [records.increment, records.task]].map(([parent, child]) => {
102
+ const parentMentionsChild = textReference(parent.body, child.number, repository);
103
+ const field = child.inspection.parent;
104
+ const referencesExpected = 'value' in field ? textReference(field.value, parent.number, repository).observed : null;
105
+ if (!parentMentionsChild.observed) findings.push({ issue: parent.number, field: 'child-text-reference', state: 'not-established', child: child.number });
106
+ if (referencesExpected !== true) findings.push({ issue: child.number, field: 'parent-text-reference', state: 'not-established', parent: parent.number });
107
+ return { parent: parent.number, child: child.number, parentMentionsChild, childParent: { ...field, referencesExpected } };
108
+ });
109
+ return { repository, projectSelection, capabilities: { project: 'not-probed', nativeLinks: 'not-probed' }, records, textLinks, findings };
110
+ }
47
111
  function items(request, bound, projectId) {
48
112
  const response = json(['api', 'graphql', '--input', '-'], JSON.stringify({ query: itemQuery, variables: { ids: [request.increment.itemId, request.task.itemId], agentFieldId: request.agentFieldId } }));
49
113
  requireValue(response.data.agentField?.dataType === 'TEXT', 'The Agent field must be a text field.');
@@ -125,18 +189,20 @@ function apply(request, inspection, config) {
125
189
  }
126
190
 
127
191
  if (process.argv[2] === '--help') {
128
- console.log('Inspect: node github-project-checkpoint.mjs inspect REQUEST.json [CONFIG.json]\nApply: node github-project-checkpoint.mjs apply REQUEST.json INSPECTION.json [CONFIG.json]');
192
+ console.log('Hierarchy: node github-project-checkpoint.mjs inspect-hierarchy REQUEST.json [CONFIG.json]\nInspect: node github-project-checkpoint.mjs inspect REQUEST.json [CONFIG.json]\nApply: node github-project-checkpoint.mjs apply REQUEST.json INSPECTION.json [CONFIG.json]');
129
193
  } else {
130
194
  try {
131
195
  const [mode, requestPath, third, fourth] = process.argv.slice(2);
132
- requireValue(mode === 'inspect' || mode === 'apply', 'Expected inspect or apply; use --help for usage.');
196
+ requireValue(['inspect', 'apply', 'inspect-hierarchy'].includes(mode), 'Expected inspect-hierarchy, inspect or apply; use --help for usage.');
133
197
  const request = load(requestPath);
134
- const config = load((mode === 'inspect' ? third : fourth) || '.agents/planning.json');
135
- const result = mode === 'inspect' ? inspect(request, config) : apply(request, load(third), config);
198
+ const config = load((mode === 'apply' ? fourth : third) || '.agents/planning.json');
199
+ const result = mode === 'inspect-hierarchy' ? inspectHierarchy(request, config) : mode === 'inspect' ? inspect(request, config) : apply(request, load(third), config);
136
200
  console.log(JSON.stringify(result));
137
201
  if (result.status && result.status !== 'synchronized') process.exitCode = 1;
138
202
  } catch (error) {
139
- console.log(JSON.stringify({ status: 'pending', reason: error.message, nextAction: 'Inspect the affected record and resolve this operation before claiming synchronization.' }));
203
+ console.log(JSON.stringify(process.argv[2] === 'inspect-hierarchy'
204
+ ? { status: 'inspection-unavailable', reason: error.message, nextAction: 'Read the affected configuration or record before claiming a completed inspection.' }
205
+ : { status: 'pending', reason: error.message, nextAction: 'Inspect the affected record and resolve this operation before claiming synchronization.' }));
140
206
  process.exitCode = 1;
141
207
  }
142
208
  }
@@ -156,7 +156,7 @@ Use this contribution lifecycle:
156
156
 
157
157
  A starting battle plan exposes likely work, dependencies and parallel opportunities without precreating, assigning or reserving every future task. The increment owner keeps acceptance, sequence and integration clear and resolves overlap/dependencies. Explicit assignments and handoffs remain valid. Two agents sharing an account are distinct contributors. Checklists suffice for routine steps; do not create a task for every command. Task creation within assessed scope requires no fresh human approval.
158
158
 
159
- **Each agent holds only one contribution task at a time**, including a blocked assignment. Finish, explicitly hand over or release the current assignment before claiming another. Do not reserve queued tasks. Outcome or increment coordination ownership does not reserve contribution tasks. Agents sharing an account remain distinct through their recorded agent/session identities.
159
+ **One contribution claim is a ceiling, not a retention requirement.** A blocked assignment still counts until its release is verified. For an unfinished task, when no useful authorized in-task step remains, hand over the remaining gap, remove your assignment and read back its removal; do not retain it merely because its dependency is unresolved. Preserve genuine assigned watcher duties; routine discussion reads alone do not justify retaining an exhausted reservation. Finish, explicitly hand over or release the current assignment before claiming another. Do not reserve queued tasks. Outcome or increment coordination ownership does not reserve contribution tasks. Agents sharing an account remain distinct through their recorded agent/session identities.
160
160
 
161
161
  Before declaring a contribution ready to start, complete this claim receipt: **no other contribution assignment held; parent increment; current assessed plan revision; non-overlapping scope; dependencies (or explicitly none); actual account; distinct agent/session; saved claim and reciprocal-link readback; competing-claim recheck.** Record all fields on the task, even when some are already known in conversation. Assignment does not increase authority. When explaining a proposed claim, make these required fields and the before/after overlap checks explicit.
162
162
 
@@ -188,21 +188,31 @@ actual acknowledgement.
188
188
 
189
189
  At each work checkpoint, reconcile tracking in this order:
190
190
 
191
- For a join/resume checkpoint with a selected Project and working `gh`, MUST use
192
- the [executable Project checkpoint](github-project-checkpoint.md). Read its
193
- inspection and handle current discussion before applying it; do not manually
194
- publish synchronization-success text on that route. Other checkpoints and
195
- authorized alternatives retain the rules below.
196
-
197
- 1. **Establish scope.** Reuse the established backend. Establish Project selection
198
- separately from the backend, using configuration, project requirements or
199
- explicit direction: selected/required, none, or unresolved. Do not infer none
200
- or an unset configuration from omitted information. An established choice to
201
- use no Project needs no setup. While selection remains unresolved, inspect available
202
- records and keep Project work conditional; do not assert a pending or
203
- unsynchronized mirror. An unavailable API or bootstrap default proves neither
204
- selection nor a missing view, field or membership; inspect saved setup before
205
- naming gaps.
191
+ When the affected records include an outcome, current Now, provisional Next and
192
+ a task, and `gh` works, MUST first use the [read-only hierarchy inspector](github-project-checkpoint.md#inspect-the-hierarchy).
193
+ Construct its request from those actual records and consume its findings before
194
+ claiming completeness; identical draft/remote text is not that comparison.
195
+ Do not manufacture a Next issue for this route. Its configuration report is one
196
+ source for the selection decision below, not proof of capability or acceptance.
197
+
198
+ 1. **Establish selection before checking Project capability.** Reuse the
199
+ established backend. From configuration, project requirements or explicit
200
+ direction, choose the applicable branch:
201
+ - **Selected/required:** verify the repository, Project and account, then
202
+ inspect its saved setup and applicable operations. For join/resume of an
203
+ active open task with working `gh`, MUST use the [executable Project checkpoint](github-project-checkpoint.md).
204
+ Read its inspection and handle current discussion before applying it;
205
+ do not manually publish synchronization-success text on that route.
206
+ If an applicable operation is unavailable, retain its exact pending state
207
+ under the recovery rules below. An unavailable API or bootstrap default
208
+ does not prove a missing view, field or membership.
209
+ - **Established none:** continue issue-side reconciliation without Project
210
+ setup, fields or synchronization.
211
+ - **Unresolved:** inspect available configuration, requirements, direction and
212
+ issue records. Record the selection gap and keep Project work conditional;
213
+ there is no established mirror operation to report pending or unsynchronized.
214
+ Omitted information and API availability establish neither selection nor
215
+ an explicit choice to use no Project.
206
216
  2. **Inspect each affected record.** Read each outcome, increment and task
207
217
  against the minimum-information table above. Preserve its own owner/account,
208
218
  applicable agent/session, revision and acceptance; mark missing required
@@ -1,6 +1,72 @@
1
- # Execute a selected-Project resume checkpoint
1
+ # Inspect and reconcile GitHub records
2
2
 
3
- For a join/resume checkpoint with a selected GitHub Project and working `gh`,
3
+ Resolve `scripts/github-project-checkpoint.mjs` relative to this installed
4
+ planning skill, including when called from `contribute`. Use the existing
5
+ authenticated `gh` account and saved project configuration. The default is
6
+ `.agents/planning.json`; an existing project-declared configuration path may be
7
+ supplied last. These operations require no new grants, account changes or tokens.
8
+ Local tracking keeps its existing path.
9
+
10
+ ## Inspect the hierarchy
11
+
12
+ When reconciling an outcome, current Now, an affected provisional Next and a
13
+ task together with working `gh`, planning and `contribute` MUST first use the
14
+ read-only hierarchy inspector. Supply those actual distinct issue numbers;
15
+ do not invent a Next issue to satisfy the command. Other record sets retain
16
+ their existing coordination path.
17
+
18
+ ```json
19
+ {
20
+ "outcome": 1,
21
+ "increment": {"issue": 2},
22
+ "next": {"issue": 3},
23
+ "task": {"issue": 4}
24
+ }
25
+ ```
26
+
27
+ Run `node <installed-planning-skill>/scripts/github-project-checkpoint.mjs inspect-hierarchy request.json`.
28
+ This reads the four actual issues and their returned comments. It writes
29
+ nothing and makes no Project or native-link request. Read the raw records and
30
+ consume `findings` before claiming that the records are correct:
31
+
32
+ - `projectSelection` reports configuration facts. `not-recorded` means selection
33
+ is unresolved from that configuration, not an explicit choice to use no
34
+ Project. `recorded` does not prove capability; consider actual project
35
+ requirements and explicit direction too. Preserve invalid configuration.
36
+ - Each record's `inspection` reports literal owner, revision, acceptance and
37
+ Parent candidates with raw line evidence. `candidate` is text, not verified
38
+ ownership or passed acceptance. `explicitly-unresolved` preserves a recorded
39
+ unknown. Empty/missing literals, unsupported syntax and multiple candidates
40
+ remain unestablished by this inspector; they do not prove semantic information
41
+ is absent or that the issue is invalid. Inspect the actual prose/history
42
+ rather than forcing a new format or choosing a historical value.
43
+ - `textLinks` compares the three requested textual relationships. A mention is
44
+ observed text, not proof of a current native relationship. Inspect both sides
45
+ and repair known reciprocal gaps under the existing coordination rules.
46
+ - `capabilities` are `not-probed`; do not turn that into an outage or native-link
47
+ absence. `inspection-unavailable` means the read/report operation failed,
48
+ not that a Project mirror is pending.
49
+
50
+ A successful command produces a diagnostic report, never a correctness or
51
+ synchronization verdict. Identical drafts and saved bodies do not establish
52
+ completeness. Before claiming completeness, account for every finding through
53
+ actual semantic review, authorized repair with saved readback where needed, or
54
+ honest unresolved recording in the existing plan or record. Follow the selection
55
+ finding's conditional next action, which targets the existing outcome issue
56
+ for saving and reading back a still-unresolved gap. Reuse an already saved gap
57
+ without an unchanged write. Configuration alone may leave the choice unresolved
58
+ while requirements or explicit direction establish it. Unsupported
59
+ syntax does not force normalization or another artifact.
60
+ Apply the existing per-record rules to known gaps, marking missing
61
+ required information unresolved rather than inventing it; preserve completed
62
+ acceptance and attribution. Read back authorized changes and compare reciprocal
63
+ links before reporting reconciliation. Unsupported syntax needs actual review,
64
+ not automatic normalization. Semantic acceptance remains with the caller and
65
+ required independent review.
66
+
67
+ ## Synchronize selected Project fields
68
+
69
+ For a join/resume checkpoint on an active open task with a selected GitHub Project and working `gh`,
4
70
  planning and `contribute` MUST use the bundled
5
71
  `scripts/github-project-checkpoint.mjs` operation. Do not manually publish a
6
72
  Project synchronization-success claim on this route. The operation reads the
@@ -8,10 +74,8 @@ records/discussion, updates the two items, verifies their saved fields and only
8
74
  then generates the task's synchronization result. It does not allocate owners,
9
75
  approve a plan, acknowledge another person or complete the increment.
10
76
 
11
- Resolve the script relative to this installed planning skill, including when
12
- called from `contribute`. Use the existing authenticated `gh` account and saved
13
- project configuration; no new grants, account changes or tokens are required by
14
- the operation. Local and GitHub Issues-only tracking keep their existing paths.
77
+ This mutation route applies to its open outcome, increment and active task.
78
+ GitHub Issues-only tracking uses no Project field mutations.
15
79
 
16
80
  ## Inspect, decide, apply
17
81
 
@@ -26,10 +26,14 @@ function json(args, input) {
26
26
  if (value.errors?.length) throw new Error(value.errors.map(error => error.message).join('; '));
27
27
  return value;
28
28
  }
29
- function scope(request, config) {
30
- requireValue(config.version === 1 && config.backend === 'github', 'This operation requires the saved GitHub backend.');
29
+ function repositoryFor(config) {
30
+ requireValue(config?.version === 1 && config.backend === 'github', 'This operation requires the saved GitHub backend.');
31
31
  const repository = config.github?.repository;
32
- requireValue(typeof repository === 'string' && /^[\w.-]+\/[\w.-]+$/.test(repository), 'Invalid configured GitHub repository.');
32
+ requireValue(typeof repository === 'string' && /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\/[\w.-]+$/.test(repository) && !['.', '..'].includes(repository.split('/')[1]), 'Invalid configured GitHub repository.');
33
+ return repository;
34
+ }
35
+ function scope(request, config) {
36
+ const repository = repositoryFor(config);
33
37
  const url = new URL(config.github?.projectUrl);
34
38
  const match = url.pathname.match(/^\/(?:users|orgs)\/([\w.-]+)\/projects\/([1-9]\d*)(?:\/views\/[1-9]\d*)?\/?$/);
35
39
  requireValue(url.protocol === 'https:' && url.hostname === 'github.com' && match && !url.username && !url.password, 'A selected GitHub Project URL is required.');
@@ -44,6 +48,66 @@ function scope(request, config) {
44
48
  function issue(number, bound) {
45
49
  return json(['issue', 'view', String(number), '--repo', bound.repository, '--json', 'number,url,body,state,assignees,labels,comments']);
46
50
  }
51
+ function briefField(body, pattern, cue) {
52
+ // shortcut: observed literal brief fields only; extend when an actual brief needs another syntax.
53
+ const lines = body.split(/\r?\n/).map((text, index) => ({ line: index + 1, text }));
54
+ const matches = lines.flatMap(line => {
55
+ const match = line.text.match(pattern);
56
+ return match ? [{ ...line, value: match[1].trim() }] : [];
57
+ });
58
+ const literalEvidence = matches.map(({ line, text }) => ({ line, text }));
59
+ if (matches.length > 1) return { state: 'multiple-candidates', evidence: literalEvidence };
60
+ if (matches.length === 1) {
61
+ const { value } = matches[0];
62
+ return { state: !value ? 'empty' : /^(?:unresolved|unknown|tbd|unclaimed)(?:\b|$)/i.test(value) ? 'explicitly-unresolved' : 'candidate', value, evidence: literalEvidence };
63
+ }
64
+ const evidence = lines.filter(line => cue.test(line.text));
65
+ return { state: evidence.length ? 'unsupported-format' : 'literal-field-missing', evidence };
66
+ }
67
+ function textReference(body, number, repository) {
68
+ const escaped = repository.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
69
+ const reference = new RegExp(`(?:(?<![\\w./#:-])#${number}\\b|https://github\\.com/${escaped}/issues/${number}(?![\\w/]))`, 'i');
70
+ const evidence = body.split(/\r?\n/).flatMap((text, index) => reference.test(text) ? [{ line: index + 1, text }] : []);
71
+ return { observed: evidence.length > 0, evidence };
72
+ }
73
+ function inspectHierarchy(request, config) {
74
+ const repository = repositoryFor(config);
75
+ const roles = { outcome: request?.outcome, increment: request?.increment?.issue, next: request?.next?.issue, task: request?.task?.issue };
76
+ requireValue(Object.values(roles).every(n => Number.isSafeInteger(n) && n > 0) && new Set(Object.values(roles)).size === 4, 'Four distinct outcome, increment, next and task issue numbers are required.');
77
+ const projectSelection = { state: 'not-recorded', source: 'configuration' };
78
+ if (Object.hasOwn(config.github, 'projectUrl')) {
79
+ try {
80
+ const url = new URL(config.github.projectUrl);
81
+ requireValue(url.protocol === 'https:' && url.hostname === 'github.com' && !url.username && !url.password && !url.search && !url.hash && /^\/(?:users|orgs)\/[\w.-]+\/projects\/[1-9]\d*(?:\/views\/[1-9]\d*)?\/?$/.test(url.pathname), 'Invalid recorded Project URL.');
82
+ Object.assign(projectSelection, { state: 'recorded', url: `https://github.com${url.pathname.split('/views/')[0].replace(/\/$/, '')}` });
83
+ } catch { projectSelection.state = 'invalid'; }
84
+ }
85
+ const records = {};
86
+ const findings = projectSelection.state === 'recorded' ? [] : [{ issue: roles.outcome, field: 'projectSelection', state: projectSelection.state, nextAction: `Establish selection from applicable project requirements and explicit direction. If it remains unresolved, save that selection gap and conditional Project work on outcome #${roles.outcome} and read back its saved body before claiming reconciliation complete. Reuse an already saved gap without an unchanged write.` }];
87
+ for (const [role, number] of Object.entries(roles)) {
88
+ const record = issue(number, { repository });
89
+ requireValue(record.number === number && record.url?.toLowerCase() === `https://github.com/${repository}/issues/${number}`.toLowerCase() && typeof record.body === 'string', 'Returned issue does not match its requested repository/number or brief.');
90
+ const inspection = {
91
+ owner: briefField(record.body, /^\s*(?:(?:Outcome|Increment|Task) )?Owner:\s*(.*)$/i, /\bowner\b/i),
92
+ revision: briefField(record.body, /^\s*Revision:\s*(.*)$/i, /\brevision\b/i),
93
+ acceptance: briefField(record.body, /^\s*(?:Tentative )?Acceptance(?: criteria| evidence)?:\s*(.*)$/i, /\bacceptance\b/i),
94
+ parent: briefField(record.body, /^\s*Parent(?: outcome| increment)?:\s*(.*)$/i, /\bparent\b/i)
95
+ };
96
+ records[role] = { ...record, inspection };
97
+ for (const field of ['owner', 'revision', 'acceptance']) {
98
+ if (!['candidate', 'explicitly-unresolved'].includes(inspection[field].state)) findings.push({ issue: number, field, state: inspection[field].state });
99
+ }
100
+ }
101
+ const textLinks = [[records.outcome, records.increment], [records.outcome, records.next], [records.increment, records.task]].map(([parent, child]) => {
102
+ const parentMentionsChild = textReference(parent.body, child.number, repository);
103
+ const field = child.inspection.parent;
104
+ const referencesExpected = 'value' in field ? textReference(field.value, parent.number, repository).observed : null;
105
+ if (!parentMentionsChild.observed) findings.push({ issue: parent.number, field: 'child-text-reference', state: 'not-established', child: child.number });
106
+ if (referencesExpected !== true) findings.push({ issue: child.number, field: 'parent-text-reference', state: 'not-established', parent: parent.number });
107
+ return { parent: parent.number, child: child.number, parentMentionsChild, childParent: { ...field, referencesExpected } };
108
+ });
109
+ return { repository, projectSelection, capabilities: { project: 'not-probed', nativeLinks: 'not-probed' }, records, textLinks, findings };
110
+ }
47
111
  function items(request, bound, projectId) {
48
112
  const response = json(['api', 'graphql', '--input', '-'], JSON.stringify({ query: itemQuery, variables: { ids: [request.increment.itemId, request.task.itemId], agentFieldId: request.agentFieldId } }));
49
113
  requireValue(response.data.agentField?.dataType === 'TEXT', 'The Agent field must be a text field.');
@@ -125,18 +189,20 @@ function apply(request, inspection, config) {
125
189
  }
126
190
 
127
191
  if (process.argv[2] === '--help') {
128
- console.log('Inspect: node github-project-checkpoint.mjs inspect REQUEST.json [CONFIG.json]\nApply: node github-project-checkpoint.mjs apply REQUEST.json INSPECTION.json [CONFIG.json]');
192
+ console.log('Hierarchy: node github-project-checkpoint.mjs inspect-hierarchy REQUEST.json [CONFIG.json]\nInspect: node github-project-checkpoint.mjs inspect REQUEST.json [CONFIG.json]\nApply: node github-project-checkpoint.mjs apply REQUEST.json INSPECTION.json [CONFIG.json]');
129
193
  } else {
130
194
  try {
131
195
  const [mode, requestPath, third, fourth] = process.argv.slice(2);
132
- requireValue(mode === 'inspect' || mode === 'apply', 'Expected inspect or apply; use --help for usage.');
196
+ requireValue(['inspect', 'apply', 'inspect-hierarchy'].includes(mode), 'Expected inspect-hierarchy, inspect or apply; use --help for usage.');
133
197
  const request = load(requestPath);
134
- const config = load((mode === 'inspect' ? third : fourth) || '.agents/planning.json');
135
- const result = mode === 'inspect' ? inspect(request, config) : apply(request, load(third), config);
198
+ const config = load((mode === 'apply' ? fourth : third) || '.agents/planning.json');
199
+ const result = mode === 'inspect-hierarchy' ? inspectHierarchy(request, config) : mode === 'inspect' ? inspect(request, config) : apply(request, load(third), config);
136
200
  console.log(JSON.stringify(result));
137
201
  if (result.status && result.status !== 'synchronized') process.exitCode = 1;
138
202
  } catch (error) {
139
- console.log(JSON.stringify({ status: 'pending', reason: error.message, nextAction: 'Inspect the affected record and resolve this operation before claiming synchronization.' }));
203
+ console.log(JSON.stringify(process.argv[2] === 'inspect-hierarchy'
204
+ ? { status: 'inspection-unavailable', reason: error.message, nextAction: 'Read the affected configuration or record before claiming a completed inspection.' }
205
+ : { status: 'pending', reason: error.message, nextAction: 'Inspect the affected record and resolve this operation before claiming synchronization.' }));
140
206
  process.exitCode = 1;
141
207
  }
142
208
  }