amsd-pipeline-shared 0.5.0 → 0.5.2
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/docs/gateway-baseline-2.0.80.md5 +47 -0
- package/docs/gateway-files.txt +47 -0
- package/orchestrations/config/repo-artifacts.json +23 -0
- package/orchestrations/config/resume-preserves.json +25 -0
- package/orchestrations/config/run-modes.json +47 -0
- package/orchestrations/scripts/lib/checkpoints.sh +175 -0
- package/orchestrations/scripts/lib/codeline-lanes.sh +1664 -0
- package/orchestrations/scripts/lib/codeline-packages.js +67 -0
- package/orchestrations/scripts/lib/codeline-write-perimeter.sh +318 -0
- package/orchestrations/scripts/lib/common.sh +66 -0
- package/orchestrations/scripts/lib/control-plane.sh +506 -0
- package/orchestrations/scripts/lib/ecosystem-registry.js +148 -0
- package/orchestrations/scripts/lib/engine-paths.sh +52 -0
- package/orchestrations/scripts/lib/flags.sh +38 -0
- package/orchestrations/scripts/lib/git-ops.sh +975 -0
- package/orchestrations/scripts/lib/halt-recovery.sh +56 -0
- package/orchestrations/scripts/lib/handlers/_read-input.js +88 -0
- package/orchestrations/scripts/lib/handlers/carry-plan.js +25 -0
- package/orchestrations/scripts/lib/handlers/cl-entries.js +10 -0
- package/orchestrations/scripts/lib/handlers/codeline-ecosystem.js +164 -0
- package/orchestrations/scripts/lib/handlers/codeline-manifests.js +148 -0
- package/orchestrations/scripts/lib/handlers/filtered-prd.js +85 -0
- package/orchestrations/scripts/lib/handlers/merge-lane-into-canonical.js +39 -0
- package/orchestrations/scripts/lib/handlers/package-engines-node.js +25 -0
- package/orchestrations/scripts/lib/handlers/prd-phases.js +46 -0
- package/orchestrations/scripts/lib/handlers/rebuild-cross-contract.js +7 -0
- package/orchestrations/scripts/lib/handlers/record-declared-pass.js +23 -0
- package/orchestrations/scripts/lib/handlers/release-line-prd.js +31 -0
- package/orchestrations/scripts/lib/handlers/release-lines.js +31 -0
- package/orchestrations/scripts/lib/handlers/repo-exclude-patterns.js +108 -0
- package/orchestrations/scripts/lib/handlers/resume-spec-output-present.js +16 -0
- package/orchestrations/scripts/lib/handlers/sibling-providers.js +43 -0
- package/orchestrations/scripts/lib/handlers/spanning-stories-incomplete.js +50 -0
- package/orchestrations/scripts/lib/handlers/spec-inputs-digest.js +48 -0
- package/orchestrations/scripts/lib/handlers/spec-predates-plan.js +40 -0
- package/orchestrations/scripts/lib/handlers/stories-lacking-spec.js +64 -0
- package/orchestrations/scripts/lib/handlers/write-missing-plans.js +92 -0
- package/orchestrations/scripts/lib/orchestration-resume.sh +74 -0
- package/orchestrations/scripts/lib/phase-exit.sh +90 -0
- package/orchestrations/scripts/lib/pipeline-commit.sh +19 -0
- package/orchestrations/scripts/lib/plan-hash.js +19 -0
- package/orchestrations/scripts/lib/plan-scope.js +73 -0
- package/orchestrations/scripts/lib/qa-gate-evidence.sh +235 -0
- package/orchestrations/scripts/lib/release-lines.sh +166 -0
- package/orchestrations/scripts/lib/resume-semantics.sh +31 -0
- package/orchestrations/scripts/lib/run-checkpoint.sh +842 -0
- package/orchestrations/scripts/lib/run-modes.sh +93 -0
- package/orchestrations/scripts/lib/story-outputs.sh +322 -0
- package/orchestrations/scripts/lib/story-retry-state.sh +355 -0
- package/package.json +1 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# WHAT THE OPERATOR IS TOLD WHEN A STORY HALTS — AND WHETHER IT IS TRUE.
|
|
3
|
+
#
|
|
4
|
+
# This ran inside run-agent-orchestration.sh, which cannot be sourced without running the whole
|
|
5
|
+
# pipeline, so the message an operator acts on had never been executed by a test.
|
|
6
|
+
#
|
|
7
|
+
# It matters because the message is a DIAGNOSIS. On a live run the writer burned 12 attempts and
|
|
8
|
+
# this printed "recovery was NOT exhausted: 0 of 12 attempt(s) used — the story failed on a gate
|
|
9
|
+
# verdict, not on running out of attempts", sending the reader to look for a gate verdict that was
|
|
10
|
+
# not the cause. A confident wrong answer costs more than no answer.
|
|
11
|
+
#
|
|
12
|
+
# Expects its caller to provide: error, read_story_retry_count, story_ladder_exhausted, MAX_RETRIES.
|
|
13
|
+
|
|
14
|
+
# _halt_recovery_state <story_id> — report the recovery state ACTUALLY observed for this story.
|
|
15
|
+
#
|
|
16
|
+
# The halt used to assert "failed after its retries and self-heal completed" and "recovery is
|
|
17
|
+
# exhausted" unconditionally. Live metrolinx AMSD-2041, 2026-08-19: a gate failed one story at
|
|
18
|
+
# attempt 2 of 12, the ladder had not left its first rung, and the writer was mid-way through
|
|
19
|
+
# addressing the reviewer's findings — and the run reported exhaustion. That message is the
|
|
20
|
+
# evidence an operator reads when deciding whether to retry, and it said the opposite of the truth.
|
|
21
|
+
#
|
|
22
|
+
# HALTING IS STILL THE MANDATE: let recovery run, then stop. What must not happen is CLAIMING
|
|
23
|
+
# exhaustion without checking. story_ladder_exhausted() already existed and was already used
|
|
24
|
+
# elsewhere in this file; the halt simply never asked it.
|
|
25
|
+
_halt_recovery_state() {
|
|
26
|
+
local _story="${1:-}"
|
|
27
|
+
local _used _max="${MAX_RETRIES:-11}"
|
|
28
|
+
_used="$(read_story_retry_count "${LOG_DIR:-}" "$_story" 2>/dev/null || echo 0)"
|
|
29
|
+
case "$_used" in (''|*[!0-9]*) _used=0 ;; esac
|
|
30
|
+
|
|
31
|
+
# A COUNT NOBODY COULD READ IS NOT A COUNT OF ZERO.
|
|
32
|
+
#
|
|
33
|
+
# read_story_retry_count answers 0 both for "no attempts" and for "I cannot see the state" —
|
|
34
|
+
# an empty LOG_DIR, or a lane whose own LOG_DIR this scope never received. On a live run the
|
|
35
|
+
# writer burned 12 attempts and this reported "0 of 12 attempt(s) used, the story failed on a
|
|
36
|
+
# gate verdict", sending the reader after a verdict that was not the cause. A confident wrong
|
|
37
|
+
# diagnosis costs more than none, so an unreadable state says so — the same rule the gate
|
|
38
|
+
# verdicts follow, where an unreadable log is a warn and never a pass.
|
|
39
|
+
local _state_file=""
|
|
40
|
+
if [ -n "${LOG_DIR:-}" ]; then
|
|
41
|
+
_state_file="$(_story_retry_state_file "$LOG_DIR" "$_story" 2>/dev/null || echo '')"
|
|
42
|
+
fi
|
|
43
|
+
if [ -z "${LOG_DIR:-}" ] || [ ! -f "$_state_file" ]; then
|
|
44
|
+
error "[orch] recovery state for '${_story}' could not be read${LOG_DIR:+ at ${LOG_DIR}}"
|
|
45
|
+
error "[orch] How many attempts were used is UNKNOWN — do not read this as none. Check the"
|
|
46
|
+
error "[orch] lane's own log directory before concluding a gate verdict stopped the story."
|
|
47
|
+
return 0
|
|
48
|
+
fi
|
|
49
|
+
if story_ladder_exhausted "${LOG_DIR:-}" "$_story" "$_max" 2>/dev/null; then
|
|
50
|
+
error "[orch] recovery is exhausted for '${_story}' — ${_used} of $((_max + 1)) attempt(s) used and the model ladder reached its top rung."
|
|
51
|
+
error "[orch] Another lane would reproduce the same failure at full ladder price."
|
|
52
|
+
else
|
|
53
|
+
error "[orch] recovery was NOT exhausted for '${_story}': ${_used} of $((_max + 1)) attempt(s) used, ladder still below its top rung."
|
|
54
|
+
error "[orch] The story failed on a gate verdict, not on running out of attempts — read the last gate message before assuming the work cannot converge."
|
|
55
|
+
fi
|
|
56
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* A HANDLER EITHER READS ITS INPUT OR SAYS IT COULD NOT.
|
|
4
|
+
*
|
|
5
|
+
* The handlers in this directory are shelled out to for decisions — how many agents a roster holds,
|
|
6
|
+
* which verdicts a gate reached, what a package.json pins. They are handed whatever the filesystem
|
|
7
|
+
* holds at that moment, which includes a file a dead step never wrote, a half-written one, and a
|
|
8
|
+
* log line where JSON was expected.
|
|
9
|
+
*
|
|
10
|
+
* Driven against those inputs on 2026-08-31, ten threw a raw node stack and eight printed a
|
|
11
|
+
* confident value anyway. roster-size.js shows why the second is worse: its own header says it
|
|
12
|
+
* "guards the skip-the-mint path", and handed unparseable JSON it printed 0 and exited 0 — so a
|
|
13
|
+
* CORRUPT roster read exactly like an EMPTY one and the guard waved the run through.
|
|
14
|
+
*
|
|
15
|
+
* A stack trace is not a diagnosis and a default is not an answer. This gives one of two outcomes:
|
|
16
|
+
* the parsed value, or a stated refusal on stderr and a non-zero exit.
|
|
17
|
+
*/
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* readJsonOrRefuse(file, what) -> parsed value, or exits non-zero having said why.
|
|
22
|
+
*
|
|
23
|
+
* `what` names the thing in the operator's terms — "the minted roster", "the AC gate results" —
|
|
24
|
+
* because "ENOENT" names a syscall and the reader has to work out the rest.
|
|
25
|
+
*
|
|
26
|
+
* An empty object or array is a REAL answer and passes through. Only json `null` is refused: every
|
|
27
|
+
* handler that took one went on to read a field from it and threw.
|
|
28
|
+
*/
|
|
29
|
+
function readJsonOrRefuse(file, what, opts) {
|
|
30
|
+
const name = what || 'its input';
|
|
31
|
+
if (!file) {
|
|
32
|
+
process.stderr.write(`[${scriptName()}] no path was given for ${name} — nothing to read\n`);
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
let text;
|
|
36
|
+
try {
|
|
37
|
+
text = fs.readFileSync(file, 'utf8');
|
|
38
|
+
} catch (e) {
|
|
39
|
+
process.stderr.write(
|
|
40
|
+
`[${scriptName()}] could not read ${name} at ${file}: ${(e && e.message) || e}\n`
|
|
41
|
+
+ ' This is not an empty result — nothing was read at all.\n');
|
|
42
|
+
process.exit(2);
|
|
43
|
+
}
|
|
44
|
+
if (!String(text).trim()) {
|
|
45
|
+
process.stderr.write(
|
|
46
|
+
`[${scriptName()}] ${name} at ${file} is EMPTY. An empty file is not an empty answer: the `
|
|
47
|
+
+ 'step that writes it may have died before it wrote anything.\n');
|
|
48
|
+
process.exit(2);
|
|
49
|
+
}
|
|
50
|
+
let value;
|
|
51
|
+
try {
|
|
52
|
+
value = JSON.parse(text);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
process.stderr.write(
|
|
55
|
+
`[${scriptName()}] ${name} at ${file} is not valid JSON and could not be parsed: `
|
|
56
|
+
+ `${(e && e.message) || e}\n`
|
|
57
|
+
+ ' Returning a default here would be indistinguishable from a real answer.\n');
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
60
|
+
if (value === null) {
|
|
61
|
+
process.stderr.write(
|
|
62
|
+
`[${scriptName()}] ${name} at ${file} parsed to null — there are no fields to read from it.\n`);
|
|
63
|
+
process.exit(2);
|
|
64
|
+
}
|
|
65
|
+
// VALID JSON OF THE WRONG SHAPE is the second half of this class. `{}` where an array was
|
|
66
|
+
// expected produced "results.forEach is not a function" — a node internal thrown mid-run, naming
|
|
67
|
+
// neither the file that was wrong nor what it should have held. A handler that knows the shape it
|
|
68
|
+
// needs should say so, and be told plainly when it does not get it.
|
|
69
|
+
const want = opts && opts.expect;
|
|
70
|
+
if (want) {
|
|
71
|
+
const got = Array.isArray(value) ? 'array' : typeof value;
|
|
72
|
+
if (want !== got) {
|
|
73
|
+
process.stderr.write(
|
|
74
|
+
`[${scriptName()}] ${name} at ${file} is an ${got}, but this reads it as an ${want}. `
|
|
75
|
+
+ 'The file parsed, so this is not a corruption — it is the wrong thing in the right place, '
|
|
76
|
+
+ 'which usually means an earlier step wrote where it should not have.\n');
|
|
77
|
+
process.exit(2);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The handler doing the reading, so a message in a shell log says which program spoke. */
|
|
84
|
+
function scriptName() {
|
|
85
|
+
try { return require('path').basename(process.argv[1] || 'handler'); } catch { return 'handler'; }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { readJsonOrRefuse };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* carry-plan.js <lane-prd> <canonical-prd> — a lane's work copy carries the story's plan.
|
|
4
|
+
*
|
|
5
|
+
* A resumed lane works from a copy of its checkpoint PRD, written before the story had a plan
|
|
6
|
+
* (write-missing-plans.js writes it into the canonical PRD). Every step inside the lane reads PRD_FILE, so the
|
|
7
|
+
* run's plan is copied onto the lane's copy of each story — replacing an older one. The checkpoint itself is never edited.
|
|
8
|
+
*/
|
|
9
|
+
'use strict';
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
|
|
12
|
+
const [laneFile, canonicalFile] = process.argv.slice(2);
|
|
13
|
+
try {
|
|
14
|
+
const lane = JSON.parse(fs.readFileSync(laneFile, 'utf8'));
|
|
15
|
+
const canonical = JSON.parse(fs.readFileSync(canonicalFile, 'utf8'));
|
|
16
|
+
let changed = false;
|
|
17
|
+
for (const s of lane.stories || []) {
|
|
18
|
+
if (!s) continue;
|
|
19
|
+
// ONE PLAN PER STORY: the run's. A lane copy's older plan is replaced (AMSD-2974, 2026-10-07: a checkpoint's
|
|
20
|
+
// older plan skipped the writer on a plan the run no longer held).
|
|
21
|
+
const c = (canonical.stories || []).find((x) => x && x.id === s.id);
|
|
22
|
+
if (c && c.overallPlan && JSON.stringify(c.overallPlan) !== JSON.stringify(s.overallPlan)) { s.overallPlan = c.overallPlan; changed = true; }
|
|
23
|
+
}
|
|
24
|
+
if (changed) fs.writeFileSync(laneFile, JSON.stringify(lane, null, 2));
|
|
25
|
+
} catch { process.exit(0); }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const p = require('./_read-input.js').readJsonOrRefuse(process.argv[2], "the run's codeline scope");
|
|
2
|
+
const dirs = p.project && p.project.outputDirs ? p.project.outputDirs : [];
|
|
3
|
+
if (dirs.length > 0) {
|
|
4
|
+
dirs.forEach(d => console.log(d.codeline + ':' + d.path));
|
|
5
|
+
} else {
|
|
6
|
+
const cl = process.env.JIRA_DEFAULT_CODELINE || '';
|
|
7
|
+
const dir = p.project && p.project.outputDir ? p.project.outputDir : '';
|
|
8
|
+
if (cl && dir) console.log(cl + ':' + dir);
|
|
9
|
+
}
|
|
10
|
+
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* WHAT ECOSYSTEM A REPOSITORY IS, AND WHAT THAT IMPLIES.
|
|
4
|
+
*
|
|
5
|
+
* codeline-health.sh assesses every codeline before a run spends anything. Its header says it
|
|
6
|
+
* "knows no package manager, no test runner and no language" — and its body named package.json,
|
|
7
|
+
* node_modules and four npm lockfiles. A Rust, Python or Ruby codeline therefore declared nothing,
|
|
8
|
+
* had nothing checked, and was reported HEALTHY without ever being assessed. A free pass from the
|
|
9
|
+
* one gate that exists to stop a run before it pays for an unusable baseline.
|
|
10
|
+
*
|
|
11
|
+
* Every answer here comes from lib/ecosystem-registry.js, the one table. This file names no manifest, no
|
|
12
|
+
* lockfile and no package manager.
|
|
13
|
+
*
|
|
14
|
+
* argv[2] the repository
|
|
15
|
+
* argv[3] optional: an estate root, to resolve a dependency to a sibling repository
|
|
16
|
+
*
|
|
17
|
+
* stdout one JSON object:
|
|
18
|
+
* stack the ecosystem label, or "" when the repo declares none
|
|
19
|
+
* manifest the manifest file found, or ""
|
|
20
|
+
* installDir where this ecosystem vendors dependencies, or null if it vendors none
|
|
21
|
+
* packageManager decided by the lockfile present, or "" when none says
|
|
22
|
+
* testCommand what runs this project's own tests, or "" when it declares none
|
|
23
|
+
* installCommand what installs its dependencies, or "" when it vendors nothing in-repo
|
|
24
|
+
* testFileCommand what runs JUST the files given in argv[4], or "" when this ecosystem cannot
|
|
25
|
+
* declaredDeps every dependency the manifest declares
|
|
26
|
+
* declaredBins the declared dependencies the repo's own scripts actually invoke
|
|
27
|
+
* missingBins of those, the ones not present in installDir
|
|
28
|
+
* providers { packageName: path } for sibling repositories in the estate
|
|
29
|
+
*
|
|
30
|
+
* A repo that declares NOTHING is not unhealthy — it has nothing to install. That is the caller's
|
|
31
|
+
* rule and this reports the facts for it to apply.
|
|
32
|
+
*/
|
|
33
|
+
'use strict';
|
|
34
|
+
|
|
35
|
+
const fs = require('fs');
|
|
36
|
+
const path = require('path');
|
|
37
|
+
|
|
38
|
+
const { allManifests, lockfileFor } = require('../ecosystem-registry.js');
|
|
39
|
+
|
|
40
|
+
const repo = process.argv[2];
|
|
41
|
+
if (!repo) {
|
|
42
|
+
process.stderr.write('[codeline-ecosystem] usage: <repo> [estate-root]\n');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
// A REPOSITORY THAT IS NOT THERE IS NOT A REPOSITORY WITHOUT A MANIFEST.
|
|
46
|
+
//
|
|
47
|
+
// Handed a path that does not exist, this returned {"stack":"","manifest":"",...} — the same
|
|
48
|
+
// answer it gives for a real checkout carrying no manifest of any known ecosystem. The caller
|
|
49
|
+
// chooses install and test commands from that, so the two must not be one answer: "there is
|
|
50
|
+
// nothing to detect here" and "I was pointed at nothing" lead to different actions.
|
|
51
|
+
const _repoStat = (() => { try { return fs.statSync(repo); } catch { return null; } })();
|
|
52
|
+
if (!_repoStat || !_repoStat.isDirectory()) {
|
|
53
|
+
process.stderr.write(
|
|
54
|
+
`[codeline-ecosystem] ${repo} is ${_repoStat ? 'not a directory' : 'not there'} — this is NOT `
|
|
55
|
+
+ 'the same as a repository with no recognised manifest, and an empty ecosystem here would be '
|
|
56
|
+
+ 'read as one. The caller picks install and test commands from this answer.\n');
|
|
57
|
+
process.exit(2);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const estate = process.argv[3] || '';
|
|
61
|
+
// argv[4]: optional comma-separated test files, to be turned into a runnable command.
|
|
62
|
+
|
|
63
|
+
const read = (p) => { try { return fs.readFileSync(p, 'utf8'); } catch { return null; } };
|
|
64
|
+
|
|
65
|
+
/** The first ecosystem whose manifest this repository carries. */
|
|
66
|
+
function ecosystemOf(root) {
|
|
67
|
+
for (const eco of allManifests()) {
|
|
68
|
+
if (fs.existsSync(path.join(root, eco.file))) return eco;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const eco = ecosystemOf(repo);
|
|
74
|
+
const out = {
|
|
75
|
+
stack: '', manifest: '', installDir: null, packageManager: '', testCommand: '', buildCommand: '', testFileCommand: '',
|
|
76
|
+
installCommand: '', addCommand: '',
|
|
77
|
+
lockfile: '',
|
|
78
|
+
declaredBins: [], declaredDeps: [], missingBins: [], providers: {},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (eco) {
|
|
82
|
+
out.stack = eco.stack;
|
|
83
|
+
out.manifest = eco.file;
|
|
84
|
+
out.installDir = eco.installDir || null;
|
|
85
|
+
|
|
86
|
+
// The package manager is what the LOCKFILE says. Unknown means we do not know how to install,
|
|
87
|
+
// so we do not pretend to — the caller declines rather than guessing a command.
|
|
88
|
+
out.lockfile = lockfileFor(eco, (f) => fs.existsSync(path.join(repo, f)));
|
|
89
|
+
out.packageManager = out.lockfile ? eco.lockfiles[out.lockfile] : '';
|
|
90
|
+
if (typeof eco.addCommand === 'function') {
|
|
91
|
+
try { out.addCommand = eco.addCommand(out.packageManager) || ''; } catch { out.addCommand = ''; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
const text = read(path.join(repo, eco.file));
|
|
96
|
+
if (text !== null) {
|
|
97
|
+
let deps = [];
|
|
98
|
+
try { deps = eco.deps(text) || []; } catch { deps = []; }
|
|
99
|
+
|
|
100
|
+
// ONLY THE TOOLING THE PROJECT ACTUALLY INVOKES: a declared dependency whose name appears in
|
|
101
|
+
// the repo's own scripts. That is the set whose absence means the project cannot run its own
|
|
102
|
+
// commands — the rest may be missing without stopping a gate.
|
|
103
|
+
let scripts = '';
|
|
104
|
+
try { scripts = Object.values(JSON.parse(text).scripts || {}).join(' '); } catch { scripts = ''; }
|
|
105
|
+
// EVERY DECLARED DEPENDENCY, not only the ones the scripts invoke. The mint needs the full
|
|
106
|
+
// list as evidence for an agent brief; declaredBins is the narrower "tooling this project
|
|
107
|
+
// actually runs" set and answers a different question.
|
|
108
|
+
out.declaredDeps = deps;
|
|
109
|
+
|
|
110
|
+
out.declaredBins = scripts
|
|
111
|
+
? deps.filter((d) => new RegExp(`(^|[^\\w/@-])${d.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\w-]|$)`).test(scripts))
|
|
112
|
+
: [];
|
|
113
|
+
|
|
114
|
+
// WHAT COMMAND RUNS ITS TESTS, from the ecosystem rather than from a runner name. Empty
|
|
115
|
+
// means the project declares none — which the caller must read as "nothing to run", never as
|
|
116
|
+
// "the tests passed".
|
|
117
|
+
if (typeof eco.testCommand === 'function') {
|
|
118
|
+
try { out.testCommand = eco.testCommand(text, out.packageManager) || ''; } catch { out.testCommand = ''; }
|
|
119
|
+
}
|
|
120
|
+
// WHAT BUILDS IT, from the same registry: a sibling that consumes this repository builds against
|
|
121
|
+
// that output (lib/codeline-lanes.sh link_sibling_builds).
|
|
122
|
+
if (typeof eco.buildCommand === 'function') {
|
|
123
|
+
try { out.buildCommand = eco.buildCommand(text, out.packageManager) || ''; } catch { out.buildCommand = ''; }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// HOW TO RUN SPECIFIC TEST FILES, from the same registry. The bug-reproduction gate needs to
|
|
127
|
+
// execute one story's test, not the whole suite, and it used to probe for Node binaries by
|
|
128
|
+
// name and skip itself entirely on anything else.
|
|
129
|
+
if (typeof eco.testFileCommand === 'function') {
|
|
130
|
+
const wanted = (process.argv[4] || '').split(',').map((f) => f.trim()).filter(Boolean);
|
|
131
|
+
if (wanted.length) {
|
|
132
|
+
try { out.testFileCommand = eco.testFileCommand(out.testCommand, wanted) || ''; }
|
|
133
|
+
catch { out.testFileCommand = ''; }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (typeof eco.installCommand === 'function') {
|
|
138
|
+
try { out.installCommand = eco.installCommand(out.packageManager) || ''; }
|
|
139
|
+
catch { out.installCommand = ''; }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (out.installDir) {
|
|
143
|
+
out.missingBins = out.declaredBins.filter((b) => !fs.existsSync(path.join(repo, out.installDir, b)));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Sibling repositories, by the name each one gives ITSELF. Nothing here knows a vendor, a scope or
|
|
149
|
+
// a repository name — it reads the manifest's own `name` through the ecosystem that defines it.
|
|
150
|
+
if (estate && fs.existsSync(estate)) {
|
|
151
|
+
for (const entry of fs.readdirSync(estate)) {
|
|
152
|
+
const dir = path.join(estate, entry);
|
|
153
|
+
try { if (!fs.statSync(dir).isDirectory()) continue; } catch { continue; }
|
|
154
|
+
const sibling = ecosystemOf(dir);
|
|
155
|
+
if (!sibling || typeof sibling.selfName !== 'function') continue;
|
|
156
|
+
const text = read(path.join(dir, sibling.file));
|
|
157
|
+
if (text === null) continue;
|
|
158
|
+
let name = '';
|
|
159
|
+
try { name = sibling.selfName(text); } catch { name = ''; }
|
|
160
|
+
if (name) out.providers[name] = dir;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
process.stdout.write(`${JSON.stringify(out)}\n`);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* codeline-manifests.js — WHAT THIS CODELINE DECLARES ABOUT ITSELF, or nothing at all.
|
|
4
|
+
*
|
|
5
|
+
* run-agent-orchestration.sh wrote three .epam/ manifests into every codeline worktree that lacked
|
|
6
|
+
* them, from heredocs asserting — of a repository it had never inspected — that its manifest is
|
|
7
|
+
* package.json, its sources are .ts/.tsx/.js, its vendor directory is node_modules, its install
|
|
8
|
+
* command is npm, and that it must carry typescript, @types/node, vitest and tsx.
|
|
9
|
+
*
|
|
10
|
+
* THAT IS NOT AN ASSUMPTION, IT IS A FABRICATION. Seventeen scripts read dependency-check.json as
|
|
11
|
+
* the codeline's OWN declaration; it is the ground truth every generic component consults. Writing
|
|
12
|
+
* it from a template defeats that genericity at the source — a Python repository is handed a
|
|
13
|
+
* document saying it is TypeScript, and everything downstream then behaves "generically" against
|
|
14
|
+
* a lie, in client-repo space, with facts nobody detected.
|
|
15
|
+
*
|
|
16
|
+
* Here the manifests are ASSEMBLED from the provider whose manifest the repository actually
|
|
17
|
+
* carries. The registry answers what it can (manifest file, vendor dirs, add-command); the
|
|
18
|
+
* provider supplies the rest under `codelineManifests`.
|
|
19
|
+
*
|
|
20
|
+
* node codeline-manifests.js <codelineRoot>
|
|
21
|
+
*
|
|
22
|
+
* stdout a JSON object keyed by filename, ready to be written into .epam/
|
|
23
|
+
* exit 0 a declaration was produced
|
|
24
|
+
* exit 1 NO provider recognises this codeline, or the one that does declares nothing — nothing is
|
|
25
|
+
* printed. The caller must report that and move on, never substitute a default. An
|
|
26
|
+
* undeclared codeline is a state to surface, not one to invent an answer for.
|
|
27
|
+
*/
|
|
28
|
+
'use strict';
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
|
|
32
|
+
const { allManifests, lockfileFor } = require('../ecosystem-registry.js');
|
|
33
|
+
|
|
34
|
+
function manifestNames(eco) {
|
|
35
|
+
return [eco.file, ...(Array.isArray(eco.alsoMatches) ? eco.alsoMatches : [])];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The provider whose manifest this repository actually carries, in declared precedence order. */
|
|
39
|
+
function resolveEcosystem(root) {
|
|
40
|
+
for (const eco of allManifests()) {
|
|
41
|
+
const present = manifestNames(eco).find((n) => {
|
|
42
|
+
try { return fs.existsSync(path.join(root, n)); } catch { return false; }
|
|
43
|
+
});
|
|
44
|
+
if (present) return { eco, present };
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The package manager the codeline's lockfile names, per the provider's own lockfile table; '' when none. */
|
|
50
|
+
function packageManagerFor(eco, root) {
|
|
51
|
+
const lock = lockfileFor(eco, (f) => {
|
|
52
|
+
try { return fs.existsSync(path.join(root, f)); } catch { return false; }
|
|
53
|
+
});
|
|
54
|
+
return (lock && (eco.lockfiles || {})[lock]) || '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function build(root) {
|
|
58
|
+
const hit = resolveEcosystem(root);
|
|
59
|
+
if (!hit) return null;
|
|
60
|
+
const { eco, present } = hit;
|
|
61
|
+
|
|
62
|
+
const declared = eco.codelineManifests || {};
|
|
63
|
+
|
|
64
|
+
// The vendored tree, from what the ecosystem says it vendors. `installDir` is the one that gets
|
|
65
|
+
// populated; `artifactDirs` is the wider set it leaves behind.
|
|
66
|
+
const vendorDirs = [...new Set([
|
|
67
|
+
...(eco.installDir ? [eco.installDir] : []),
|
|
68
|
+
...(Array.isArray(eco.artifactDirs) ? eco.artifactDirs : []),
|
|
69
|
+
])];
|
|
70
|
+
|
|
71
|
+
// THE ADD COMMAND, not the install command. `installCommand` provisions what the manifest already
|
|
72
|
+
// declares and cannot add anything; the engine's heredoc wrote "npm install --save-dev {package}"
|
|
73
|
+
// beside the provider's own addCommand — two spellings of one fact.
|
|
74
|
+
const manager = packageManagerFor(eco, root);
|
|
75
|
+
let addCommand = '';
|
|
76
|
+
if (typeof eco.addCommand === 'function') {
|
|
77
|
+
try { addCommand = String(eco.addCommand(manager) || ''); } catch { addCommand = ''; }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// HOW THE ENVIRONMENT IS PROVISIONED, AND WHERE COMMANDS RUN. Two more facts the plug-in
|
|
81
|
+
// declares and the engine only executes. Until 2026-09-16 the manifest carried the ADD command
|
|
82
|
+
// alone, and the engine provisioned a worktree by deleting `{package}` from it and running the
|
|
83
|
+
// remainder — a rule true of one ecosystem's package manager and of no other: a Python worktree
|
|
84
|
+
// got `pip install` with no arguments, no interpreter, and every test exited 2.
|
|
85
|
+
let provisionCommand = '';
|
|
86
|
+
if (typeof eco.installCommand === 'function') {
|
|
87
|
+
try { provisionCommand = String(eco.installCommand(manager) || ''); } catch { provisionCommand = ''; }
|
|
88
|
+
}
|
|
89
|
+
const runEnvironment = (eco.runEnvironment && typeof eco.runEnvironment === 'object') ? eco.runEnvironment : null;
|
|
90
|
+
// Files the ecosystem says are complete when empty (a package marker), by basename.
|
|
91
|
+
const emptyDeliverables = Array.isArray(eco.emptyDeliverables) ? eco.emptyDeliverables.filter((x) => typeof x === 'string' && x) : [];
|
|
92
|
+
|
|
93
|
+
// THE RUNTIME'S BUILT-IN MODULES, ASKED OF THE RUNTIME. An ecosystem declares HOW to ask
|
|
94
|
+
// (builtinModulesCommand); the answer is carried as data (builtinModules) so the scan plug-in
|
|
95
|
+
// — which must never run a process — reads a list. Python's `__future__` was reported as an
|
|
96
|
+
// undeclared import and failed a story through the ladder (regintel 20260916T200108Z).
|
|
97
|
+
let builtinModules = [];
|
|
98
|
+
const bmc = declared.dependencyCheck && typeof declared.dependencyCheck.builtinModulesCommand === 'string'
|
|
99
|
+
? declared.dependencyCheck.builtinModulesCommand.trim() : '';
|
|
100
|
+
if (bmc) {
|
|
101
|
+
try {
|
|
102
|
+
builtinModules = require('node:child_process').execSync(bmc, { encoding: 'utf8', timeout: 20000, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
103
|
+
.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
104
|
+
} catch { builtinModules = []; }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const dependencyCheck = {
|
|
108
|
+
manifestFile: present,
|
|
109
|
+
...(declared.dependencyCheck || {}),
|
|
110
|
+
...(builtinModules.length ? { builtinModules } : {}),
|
|
111
|
+
vendorDirs,
|
|
112
|
+
...(addCommand ? { installCommand: addCommand } : {}),
|
|
113
|
+
...(provisionCommand ? { provisionCommand } : {}),
|
|
114
|
+
...(runEnvironment ? { runEnvironment } : {}),
|
|
115
|
+
...(emptyDeliverables.length ? { emptyDeliverables } : {}),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const out = { 'dependency-check.json': dependencyCheck };
|
|
119
|
+
if (declared.contractGeneration) out['contract-generation.json'] = declared.contractGeneration;
|
|
120
|
+
if (declared.knownFixes) out['known-fixes.json'] = declared.knownFixes;
|
|
121
|
+
|
|
122
|
+
// A provider that supplies nothing beyond its own identity has not declared how this codeline is
|
|
123
|
+
// checked. Emitting a near-empty dependency-check.json would read downstream as a real
|
|
124
|
+
// declaration, which is the defect this file exists to remove.
|
|
125
|
+
if (!declared.dependencyCheck) return null;
|
|
126
|
+
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function main() {
|
|
131
|
+
const root = process.argv[2];
|
|
132
|
+
if (!root) {
|
|
133
|
+
process.stderr.write('[codeline-manifests] usage: <codelineRoot>\n');
|
|
134
|
+
return 2;
|
|
135
|
+
}
|
|
136
|
+
const out = build(root);
|
|
137
|
+
if (!out) {
|
|
138
|
+
process.stderr.write(
|
|
139
|
+
`[codeline-manifests] no provider declares how ${root} is checked — writing nothing. `
|
|
140
|
+
+ 'Its ecosystem must declare this, or supply a provider that does.\n');
|
|
141
|
+
return 1;
|
|
142
|
+
}
|
|
143
|
+
process.stdout.write(JSON.stringify(out, null, 2) + '\n');
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (require.main === module) process.exit(main());
|
|
148
|
+
module.exports = { build, resolveEcosystem, packageManagerFor };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const prd = require('./_read-input.js').readJsonOrRefuse(process.argv[2], 'the PRD', { expect: 'object' });
|
|
3
|
+
const cl = process.argv[4], dcl = process.env.JIRA_DEFAULT_CODELINE||'';
|
|
4
|
+
// A story may SPAN codelines. codelines[] is authoritative when present:
|
|
5
|
+
// the story stays whole and participates in each lane's execution, rather
|
|
6
|
+
// than being partitioned into exactly one. Without this it matches no
|
|
7
|
+
// partition, appears in zero filtered PRDs, and is silently dropped from
|
|
8
|
+
// the run — which is how a [GO, UP, MX] ticket reached ingest and died.
|
|
9
|
+
// The top-level shape is checked on read; a PRD that parsed but carries no stories array
|
|
10
|
+
// still reaches here, and `.filter` on undefined is a node internal thrown mid-run.
|
|
11
|
+
const stories = (prd.stories || []).filter(s =>
|
|
12
|
+
(Array.isArray(s.codelines) && s.codelines.length
|
|
13
|
+
? s.codelines.includes(cl)
|
|
14
|
+
: (s.codeline === cl || (!s.codeline && cl === dcl)))
|
|
15
|
+
// THIS LANE'S PRD SAYS WHICH LANE IT IS.
|
|
16
|
+
//
|
|
17
|
+
// Stories were copied through unchanged, so a story spanning three codelines carried
|
|
18
|
+
// its PRIMARY codeline into all three lane PRDs. Every consumer reading the singular
|
|
19
|
+
// field then got the same answer everywhere: the detective resolved the first lane's
|
|
20
|
+
// investigator in all three lanes and investigated two repositories with a brief written
|
|
21
|
+
// for a different one. agentRole had the identical defect, and project.outputDir vs
|
|
22
|
+
// outputDirs a third instance.
|
|
23
|
+
//
|
|
24
|
+
// Fixed HERE rather than in each reader. A lane's PRD describing a different lane is the
|
|
25
|
+
// lie; once it tells the truth, every consumer — the detective, the guards, the manifest,
|
|
26
|
+
// whatever is added next — is correct without knowing lanes exist. codelines[] is left
|
|
27
|
+
// intact, so nothing loses the knowledge that the story spans more than this one.
|
|
28
|
+
// THIS LANE'S CRITERIA, NOT THE UNION.
|
|
29
|
+
//
|
|
30
|
+
// verificationCriteria / fixSiteAnalysis on canonical are the UNION across every lane —
|
|
31
|
+
// that is what canonical is for. Copying them through means a lane is handed other
|
|
32
|
+
// codelines' criteria, describing files that do not exist in its checkout.
|
|
33
|
+
//
|
|
34
|
+
// On a first run the spec pass overwrote them before anything read them, so it never
|
|
35
|
+
// showed. On a RESUME the spec pass is skipped — the whole point of resuming — so the
|
|
36
|
+
// lane keeps the union, its writer and gates verify against it, and
|
|
37
|
+
// mergeLaneIntoCanonical then faithfully records the union as THIS LANE'S OWN criteria.
|
|
38
|
+
// Live 2026-08-09: four killed runs turned gotransit's 4 criteria into all 14, and 13 fix
|
|
39
|
+
// sites into 22, compounding on every kill.
|
|
40
|
+
//
|
|
41
|
+
// Same rule as `codeline` above, and for the same reason: a lane's PRD states that lane's
|
|
42
|
+
// truth. Falls back to the flat list when there is no entry for this codeline (a first
|
|
43
|
+
// run, or a lane added later) — never to nothing, which would hand a writer an empty
|
|
44
|
+
// plan. An explicitly EMPTY entry is honoured, because 'this lane found nothing to
|
|
45
|
+
// verify' is a real state and differs from 'this lane has not run'.
|
|
46
|
+
//
|
|
47
|
+
// No double quotes anywhere in this block, comments included: the node script is passed
|
|
48
|
+
// inside a double-quoted shell string, so one would end it and bash would execute the
|
|
49
|
+
// JavaScript that follows.
|
|
50
|
+
).map(s => {
|
|
51
|
+
const _scoped = (flat, perCl) =>
|
|
52
|
+
(perCl && Object.prototype.hasOwnProperty.call(perCl, cl)) ? perCl[cl] : flat;
|
|
53
|
+
const _ownText = (s.specTextPerCodeline && s.specTextPerCodeline[cl]) || null;
|
|
54
|
+
return {
|
|
55
|
+
...s,
|
|
56
|
+
// THIS LANE'S OWN SPEC TEXT, OR THE TICKET'S - never another lane's rewrite (story-merge.js).
|
|
57
|
+
...(_ownText ? { title: _ownText.title, description: _ownText.description } : {}),
|
|
58
|
+
codeline: cl,
|
|
59
|
+
verificationCriteria: _scoped(s.verificationCriteria, s.verificationCriteriaPerCodeline),
|
|
60
|
+
// A LANE NEVER INHERITS ANOTHER CODELINE'S FIX SITES. With no entry of its own yet, the
|
|
61
|
+
// fallback keeps only findings tagged with this codeline: the next lane of a spanning
|
|
62
|
+
// story was handed the first lane's files, which do not exist in its checkout (AMSD-2974,
|
|
63
|
+
// 2026-10-01). Findings carry their codeline once pooled.
|
|
64
|
+
fixSiteAnalysis: (s.fixSiteAnalysisPerCodeline && Object.prototype.hasOwnProperty.call(s.fixSiteAnalysisPerCodeline, cl))
|
|
65
|
+
? s.fixSiteAnalysisPerCodeline[cl]
|
|
66
|
+
: (Array.isArray(s.codelines) && s.codelines.length > 1 && Array.isArray(s.fixSiteAnalysis)
|
|
67
|
+
? s.fixSiteAnalysis.filter(f => f && f.codeline === cl)
|
|
68
|
+
: s.fixSiteAnalysis),
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
const ids = new Set(stories.map(s => s.id));
|
|
72
|
+
const order = {};
|
|
73
|
+
for (const [phase, list] of Object.entries(prd.implementationOrder||{})) {
|
|
74
|
+
const f = (list||[]).filter(id => ids.has(id));
|
|
75
|
+
if (f.length > 0) order[phase] = f;
|
|
76
|
+
}
|
|
77
|
+
if (Object.keys(order).length === 0) order.core = stories.map(s => s.id);
|
|
78
|
+
const out = {...prd, stories, implementationOrder: order};
|
|
79
|
+
if (prd.project && prd.project.outputDirs) {
|
|
80
|
+
const d = prd.project.outputDirs.find(d => d.codeline === cl);
|
|
81
|
+
if (d) out.project = {...prd.project, outputDir: d.path};
|
|
82
|
+
}
|
|
83
|
+
fs.writeFileSync(process.argv[3], JSON.stringify(out, null, 2));
|
|
84
|
+
console.log(stories.length);
|
|
85
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MERGE ONE LANE'S STORY STATE BACK INTO THE CANONICAL PRD.
|
|
4
|
+
*
|
|
5
|
+
* Lanes of a spanning story run in parallel and each writes only its own filtered PRD. The merges
|
|
6
|
+
* happen afterwards, one at a time, here — concurrent merges into the same file would clobber
|
|
7
|
+
* each other.
|
|
8
|
+
*
|
|
9
|
+
* Lifted out of run-agent-orchestration.sh on 2026-08-16, where it was a `node -e "..."` string
|
|
10
|
+
* that interpolated four shell values directly into its own source. A codeline name or path
|
|
11
|
+
* containing a quote was a syntax error in a language the author was not writing.
|
|
12
|
+
*
|
|
13
|
+
* Generic: every input is an argument, and the rule holds for any project and any stack.
|
|
14
|
+
*
|
|
15
|
+
* argv[2] SCRIPT_DIR — where lib/story-merge.js lives
|
|
16
|
+
* argv[3] the canonical PRD, read and written in place
|
|
17
|
+
* argv[4] this lane's filtered PRD
|
|
18
|
+
* argv[5] the codeline name this lane ran as
|
|
19
|
+
*
|
|
20
|
+
* Exits non-zero on any failure. The caller distinguishes that from success and says so: a failed
|
|
21
|
+
* merge silently discards a lane's entire outcome — status, criteria and all.
|
|
22
|
+
*/
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
|
|
28
|
+
const [, , scriptDir, canonicalPath, updatedPath, codeline] = process.argv;
|
|
29
|
+
if (!scriptDir || !canonicalPath || !updatedPath || !codeline) {
|
|
30
|
+
process.stderr.write('[merge-lane-into-canonical] usage: <script-dir> <canonical-prd> <lane-prd> <codeline>\n');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { mergeLaneIntoCanonical } = require(path.join(scriptDir, 'lib/story-merge.js'));
|
|
35
|
+
|
|
36
|
+
const canonical = JSON.parse(fs.readFileSync(canonicalPath, 'utf8'));
|
|
37
|
+
const updated = JSON.parse(fs.readFileSync(updatedPath, 'utf8'));
|
|
38
|
+
mergeLaneIntoCanonical({ canonical, updated, codeline });
|
|
39
|
+
fs.writeFileSync(canonicalPath, JSON.stringify(canonical, null, 2));
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* THE NODE VERSION A package.json REQUIRES, IF IT SAYS.
|
|
4
|
+
*
|
|
5
|
+
* Read before choosing an interpreter: a project that declares an engines.node range gets that
|
|
6
|
+
* version resolved through fnm rather than whatever `node` happens to be on PATH.
|
|
7
|
+
*
|
|
8
|
+
* argv[2] a package.json
|
|
9
|
+
* stdout the engines.node value, or empty when there is none
|
|
10
|
+
*
|
|
11
|
+
* Empty is the honest answer for "no requirement declared", and the caller reads it that way —
|
|
12
|
+
* it falls back to detection. An unreadable or absent file is the same answer for the same
|
|
13
|
+
* reason: it declares no requirement. This is the one place where quiet is correct, because the
|
|
14
|
+
* caller has a real fallback and the value is advisory.
|
|
15
|
+
*
|
|
16
|
+
* Lifted out of run-agent-orchestration.sh on 2026-08-16.
|
|
17
|
+
*/
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const p = require(require('path').resolve(process.argv[2]));
|
|
22
|
+
process.stdout.write((p.engines && p.engines.node) || '');
|
|
23
|
+
} catch {
|
|
24
|
+
process.stdout.write('');
|
|
25
|
+
}
|